mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-22 21:41:37 +00:00
Enhance documentation and update syncback features
- Updated the `audit.md` documentation to clarify the syncback process and the structure of the `FsSnapshot`. - Revised the `atlas-api.mdc` to reflect changes in the Git metadata handling, including the introduction of a separate endpoint for fetching Git metadata. - Improved the `atlas-dedup.mdc` to include handling for the reserved name "init" during slugification. - Enhanced the `atlas-syncback.mdc` to document the new `SyncbackResult` structure and its integration with the syncback process. - Updated various sections across multiple documentation files to ensure consistency and clarity regarding the project's architecture and API endpoints. These changes aim to improve the overall understanding of the system and its components, facilitating better usage and integration.
This commit is contained in:
parent
652e3b2f72
commit
e8dc9b09b9
8 changed files with 284 additions and 120 deletions
|
|
@ -43,7 +43,7 @@ One-shot command: reads a Roblox binary/XML file, diffs it against the existing
|
||||||
CLI Entry
|
CLI Entry
|
||||||
└── src/cli/syncback.rs Entry point. Loads project, reads .rbxl/.rbxm,
|
└── src/cli/syncback.rs Entry point. Loads project, reads .rbxl/.rbxm,
|
||||||
creates ServeSession for existing tree, calls
|
creates ServeSession for existing tree, calls
|
||||||
syncback_loop(), writes FsSnapshot to disk.
|
syncback_loop(), writes result.fs_snapshot to disk.
|
||||||
|
|
||||||
Syncback Core
|
Syncback Core
|
||||||
├── src/syncback/mod.rs Main orchestration. syncback_loop() runs the full
|
├── src/syncback/mod.rs Main orchestration. syncback_loop() runs the full
|
||||||
|
|
@ -60,8 +60,9 @@ Syncback Core
|
||||||
│ .meta.json5 paths for scripts (strips .server/.client
|
│ .meta.json5 paths for scripts (strips .server/.client
|
||||||
│ suffixes from stem).
|
│ 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, Value, Contents via reflection DB
|
||||||
│ (LocalizationTable), Ref/UniqueId, unscriptable props.
|
│ (DoesNotSerialize). Explicitly filters Ref/UniqueId
|
||||||
|
│ by variant type. Unscriptable props if configured.
|
||||||
├── src/syncback/ref_properties.rs Ref property resolution. collect_referents() finds all
|
├── src/syncback/ref_properties.rs Ref property resolution. collect_referents() finds all
|
||||||
│ Ref properties. link_referents() writes Rojo_Ref_*
|
│ Ref properties. link_referents() writes Rojo_Ref_*
|
||||||
│ (path-based) and Rojo_Id attributes to instances.
|
│ (path-based) and Rojo_Id attributes to instances.
|
||||||
|
|
@ -289,10 +290,12 @@ Patch Application (Plugin)
|
||||||
│ Used for confirmation dialog to show what will change.
|
│ Used for confirmation dialog to show what will change.
|
||||||
├── plugin/src/Reconciler/matching.lua Recursive change-count scoring algorithm. matchChildren()
|
├── plugin/src/Reconciler/matching.lua Recursive change-count scoring algorithm. matchChildren()
|
||||||
│ pairs virtual instances with Studio instances during
|
│ pairs virtual instances with Studio instances during
|
||||||
│ hydration. Signature: (virtualChildren, studioChildren,
|
│ hydration. Signature: (session, virtualChildren,
|
||||||
│ virtualInstances). One of 3 parallel matching impls that
|
│ studioChildren, virtualInstances, parentVirtualId?,
|
||||||
│ must produce identical pairings (see also src/snapshot/
|
│ parentStudioInstance?, _depth?). One of 3 parallel
|
||||||
│ matching.rs and src/syncback/matching.rs).
|
│ matching impls that must produce identical pairings
|
||||||
|
│ (see also src/snapshot/matching.rs and src/syncback/
|
||||||
|
│ matching.rs).
|
||||||
├── plugin/src/Reconciler/trueEquals.lua Shared value equality. Fuzzy floats (epsilon 0.0001),
|
├── plugin/src/Reconciler/trueEquals.lua Shared value equality. Fuzzy floats (epsilon 0.0001),
|
||||||
│ Color3 via RGB ints, CFrame/Vector3 component-wise, NaN
|
│ Color3 via RGB ints, CFrame/Vector3 component-wise, NaN
|
||||||
│ handling, nil/null-ref equivalence. Used by matching.lua
|
│ handling, nil/null-ref equivalence. Used by matching.lua
|
||||||
|
|
@ -457,7 +460,7 @@ Plugin Shared Modules
|
||||||
├── 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/)
|
Integration Tests (entry point: tests/end_to_end.rs)
|
||||||
├── tests/tests/build.rs Build tests (gen_build_tests! macro).
|
├── tests/tests/build.rs Build tests (gen_build_tests! macro).
|
||||||
├── tests/tests/serve.rs Forward-sync serve tests.
|
├── tests/tests/serve.rs Forward-sync serve tests.
|
||||||
├── tests/tests/connected_mode.rs Connected mode tests.
|
├── tests/tests/connected_mode.rs Connected mode tests.
|
||||||
|
|
@ -474,6 +477,7 @@ Integration Tests (tests/tests/)
|
||||||
├── tests/tests/git_sync_defaults.rs Git-based sync default selection tests.
|
├── tests/tests/git_sync_defaults.rs Git-based sync default selection tests.
|
||||||
│ Hash computation parity, default selection logic,
|
│ Hash computation parity, default selection logic,
|
||||||
│ auto-staging flow, no-git-repo fallback.
|
│ auto-staging flow, no-git-repo fallback.
|
||||||
|
├── tests/tests/scripts_only_pruning.rs Scripts-only mode pruning tests.
|
||||||
└── tests/tests/matching_fixtures.rs Matching algorithm fixture tests across all three
|
└── tests/tests/matching_fixtures.rs Matching algorithm fixture tests across all three
|
||||||
implementations (Rust syncback, Rust forward, Lua plugin).
|
implementations (Rust syncback, Rust forward, Lua plugin).
|
||||||
```
|
```
|
||||||
|
|
@ -784,7 +788,7 @@ The matching algorithm exists in **3 parallel implementations** that must produc
|
||||||
|
|
||||||
The confirmation dialog uses git metadata for smart default selections (`PatchTree.lua` + `src/git.rs`). Verify:
|
The confirmation dialog uses git metadata for smart default selections (`PatchTree.lua` + `src/git.rs`). Verify:
|
||||||
|
|
||||||
- **Server-side `compute_git_metadata()`**: Does it correctly identify changed files vs HEAD? Does the two-phase tree lock (snapshot paths briefly, release before git subprocesses) prevent deadlocks?
|
- **Server-side `compute_git_metadata()`**: Does it correctly identify changed files vs HEAD? Does the three-phase approach (1: run git commands for changed files with no lock, 2: briefly lock tree to resolve paths to Refs, 3: run `git cat-file --batch-check` with no lock) prevent deadlocks?
|
||||||
- **Hash computation parity**: Server uses `compute_blob_sha1()` in `git.rs` and plugin uses `SHA1.luau` -- both must produce identical hashes for the same content using `SHA1("blob <byte_len>\0<content>")` git blob format.
|
- **Hash computation parity**: Server uses `compute_blob_sha1()` in `git.rs` and plugin uses `SHA1.luau` -- both must produce identical hashes for the same content using `SHA1("blob <byte_len>\0<content>")` git blob format.
|
||||||
- **Default selection logic** (`PatchTree.lua`): File has no git changes → default "pull". File has git changes AND is script AND Studio Source matches committed hash → default "push". Otherwise → `nil` (user must decide). Verify these rules are applied correctly.
|
- **Default selection logic** (`PatchTree.lua`): File has no git changes → default "pull". File has git changes AND is script AND Studio Source matches committed hash → default "push". Otherwise → `nil` (user must decide). Verify these rules are applied correctly.
|
||||||
- **Auto-staging (`stageIds`)**: After confirmation, `api.rs` receives `stageIds` and calls `git_add`. Push-accepted items are always staged. Pull-accepted items are only staged if auto-selected (`defaultSelection ~= nil`). Manually-chosen pulls are left unstaged. Is this flow correct?
|
- **Auto-staging (`stageIds`)**: After confirmation, `api.rs` receives `stageIds` and calls `git_add`. Push-accepted items are always staged. Pull-accepted items are only staged if auto-selected (`defaultSelection ~= nil`). Manually-chosen pulls are left unstaged. Is this flow correct?
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ We are building a maintainable codebase. During audits, watch for duplicated log
|
||||||
|
|
||||||
The goal is clean, DRY code that is easy to reason about and hard to get wrong. If the same "compute meta path, check if name needs slugify, upsert/remove name field" sequence appears in 3+ places, that's a refactor candidate.
|
The goal is clean, DRY code that is easy to reason about and hard to get wrong. If the same "compute meta path, check if name needs slugify, upsert/remove name field" sequence appears in 3+ places, that's a refactor candidate.
|
||||||
|
|
||||||
**Protocol Version:** 6 | Server/plugin MAJOR.MINOR must match
|
**Protocol Version:** 6 | Plugin WebSocket requires exact version match; live syncback uses MAJOR.MINOR
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|
@ -47,12 +47,17 @@ Studio → ChangeBatcher → Plugin → WebSocket → Server → Filesystem
|
||||||
| `/api/ref-patch/:id` | GET | Fix Ref properties after syncback |
|
| `/api/ref-patch/:id` | GET | Fix Ref properties after syncback |
|
||||||
| `/api/syncback` | POST | Live syncback: full Roblox → filesystem sync |
|
| `/api/syncback` | POST | Live syncback: full Roblox → filesystem sync |
|
||||||
| `/api/socket/:cursor` | GET | WebSocket upgrade for live patch streaming |
|
| `/api/socket/:cursor` | GET | WebSocket upgrade for live patch streaming |
|
||||||
|
| `/api/git-metadata` | GET | Git metadata (changedIds, scriptCommittedHashes, newFileIds) |
|
||||||
|
| `/api/validate-tree` | GET | Tree freshness check (test infra) |
|
||||||
|
| `/api/mcp/stream` | GET | WebSocket upgrade for MCP plugin stream |
|
||||||
|
| `/api/mcp/syncback` | POST | MCP syncback (runs inline, returns stats without server restart) |
|
||||||
|
| `/mcp` | POST | MCP JSON-RPC endpoint (initialize, tools/list, tools/call) |
|
||||||
|
|
||||||
### Server Info Response (`GET /api/rojo`)
|
### Server Info Response (`GET /api/rojo`)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"serverVersion": "7.7.0-rc.1",
|
"serverVersion": "8.5.0",
|
||||||
"serverFork": "atlas",
|
"serverFork": "atlas",
|
||||||
"protocolVersion": 6,
|
"protocolVersion": 6,
|
||||||
"sessionId": "uuid",
|
"sessionId": "uuid",
|
||||||
|
|
@ -61,24 +66,36 @@ Studio → ChangeBatcher → Plugin → WebSocket → Server → Filesystem
|
||||||
"unexpectedPlaceIds": [111111],
|
"unexpectedPlaceIds": [111111],
|
||||||
"rootInstanceId": "00000000-0000-0000-0000-000000000000",
|
"rootInstanceId": "00000000-0000-0000-0000-000000000000",
|
||||||
"syncSourceOnly": true,
|
"syncSourceOnly": true,
|
||||||
|
"syncScriptsOnly": false,
|
||||||
"visibleServices": ["Workspace", "ServerScriptService"],
|
"visibleServices": ["Workspace", "ServerScriptService"],
|
||||||
"placeId": 123456,
|
"placeId": 123456,
|
||||||
"gameId": 789012,
|
"gameId": 789012,
|
||||||
"ignoreHiddenServices": true,
|
"ignoreHiddenServices": true,
|
||||||
"gitMetadata": {
|
"gitMetadata": null
|
||||||
"changedIds": ["ref-1", "ref-2"],
|
}
|
||||||
"scriptCommittedHashes": {
|
```
|
||||||
"ref-1": ["a1b2c3...40-char-sha1-hex", "d4e5f6...staged-sha1"]
|
|
||||||
}
|
**`gitMetadata`** is always `null` in the `/api/rojo` response. Git metadata is fetched separately via `GET /api/git-metadata`.
|
||||||
}
|
|
||||||
|
### Git Metadata Endpoint (`GET /api/git-metadata`)
|
||||||
|
|
||||||
|
Returns `Option<GitMetadata>` (msgpack). `null` when not in a git repo. Called by the plugin after connect and on debounced refresh.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"changedIds": ["ref-1", "ref-2"],
|
||||||
|
"scriptCommittedHashes": {
|
||||||
|
"ref-1": ["a1b2c3...40-char-sha1-hex", "d4e5f6...staged-sha1"]
|
||||||
|
},
|
||||||
|
"newFileIds": ["ref-3"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**`gitMetadata`** is `null` when the project is not in a git repo. Contains:
|
|
||||||
- `changedIds`: Instance Refs whose backing files have changes relative to HEAD (staged + unstaged + untracked)
|
- `changedIds`: Instance Refs whose backing files have changes relative to HEAD (staged + unstaged + untracked)
|
||||||
- `scriptCommittedHashes`: For changed script instances, 1-2 SHA1 hashes (HEAD version and staged version if different) computed using the git blob format (`"blob <size>\0<content>"`). Plugin checks if Studio `script.Source` matches ANY hash to default "Atlas".
|
- `scriptCommittedHashes`: For changed script instances, 1-2 SHA1 hashes (HEAD version and staged version if different) computed using the git blob format (`"blob <size>\0<content>"`). Plugin checks if Studio `script.Source` matches ANY hash to default "Atlas".
|
||||||
|
- `newFileIds`: Instance Refs whose backing files have never been committed. Used by the plugin to default new-file additions to "push".
|
||||||
|
|
||||||
Recomputed fresh on each `GET /api/rojo` connect. Uses a two-phase tree lock: snapshot path mappings briefly, then release before running git subprocesses.
|
Uses a three-phase approach: (1) run git commands for changed files scoped to project prefixes (no lock), (2) briefly lock tree to resolve paths to Refs and class names, (3) run `git cat-file --batch-check` for blob hashes (no lock). 10-second timeout. Uses `project_prefixes` (relevant `$path` roots relative to repo root) to scope `git status`/`git diff` to project directories.
|
||||||
|
|
||||||
### Write Request (`POST /api/write`)
|
### Write Request (`POST /api/write`)
|
||||||
|
|
||||||
|
|
@ -195,29 +212,36 @@ Serve loop restarts (new session, ready for connections)
|
||||||
|
|
||||||
1. Connect → Receive `ServerInfo` handshake → Send `{type = "Subscribe"}` → Receive patches
|
1. Connect → Receive `ServerInfo` handshake → Send `{type = "Subscribe"}` → Receive patches
|
||||||
|
|
||||||
### Patch Message (`ApplyPatch`)
|
### Patch Message (SocketPacket)
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
{
|
{
|
||||||
type = "ApplyPatch",
|
|
||||||
sessionId = "uuid",
|
sessionId = "uuid",
|
||||||
patch = {
|
packetType = "messages",
|
||||||
removed = {"id-1", "id-2"},
|
body = {
|
||||||
added = {
|
messageCursor = 5,
|
||||||
["id-3"] = {
|
messages = {
|
||||||
id = "id-3", className = "Folder", name = "New",
|
{
|
||||||
properties = {}, metadata = { ignoreUnknownInstances = false },
|
removed = {"id-1", "id-2"},
|
||||||
children = {"child-1"},
|
added = {
|
||||||
|
["id-3"] = {
|
||||||
|
Id = "id-3", ClassName = "Folder", Name = "New",
|
||||||
|
Properties = {}, Metadata = { ignoreUnknownInstances = false },
|
||||||
|
Children = {"child-1"}, Parent = "parent-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updated = {
|
||||||
|
{ id = "id-4", changedName = "Renamed", changedProperties = { Source = "code" } },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
updated = {
|
|
||||||
{ id = "id-4", changedName = "Renamed", changedProperties = { Source = "code" } },
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Format:** MessagePack (~30% smaller than JSON, ~2x faster)
|
**Note:** `added` Instance fields use **PascalCase** (`Id`, `Parent`, `Name`, `ClassName`, `Properties`, `Children`, `Metadata`). `updated` InstanceUpdate fields use **camelCase** (`id`, `changedName`, `changedClassName`, `changedProperties`, `changedMetadata`).
|
||||||
|
|
||||||
|
**Format:** MessagePack (~30% smaller than JSON, ~2x faster). Plugin handler: `["messages"] = function(messagesPacket)`, iterates `messagesPacket.messages` and merges into a combined patch via `PatchSet.assign`.
|
||||||
|
|
||||||
## Server ServeSession (`src/serve_session.rs`)
|
## Server ServeSession (`src/serve_session.rs`)
|
||||||
|
|
||||||
|
|
@ -228,16 +252,19 @@ pub struct ServeSession {
|
||||||
message_queue: Arc<MessageQueue<AppliedPatchSet>>,
|
message_queue: Arc<MessageQueue<AppliedPatchSet>>,
|
||||||
root_project: Project,
|
root_project: Project,
|
||||||
vfs: Arc<Vfs>,
|
vfs: Arc<Vfs>,
|
||||||
change_processor: ChangeProcessor,
|
change_processor: Option<ChangeProcessor>,
|
||||||
start_time: Instant,
|
start_time: Instant,
|
||||||
tree_mutation_sender: Sender<PatchSet>,
|
tree_mutation_sender: Option<Sender<PatchSet>>,
|
||||||
suppressed_paths: Arc<Mutex<HashMap<PathBuf, (usize, usize)>>>,
|
suppressed_paths: Option<Arc<Mutex<HashMap<PathBuf, (usize, usize)>>>>,
|
||||||
|
ref_path_index: Option<Arc<Mutex<RefPathIndex>>>,
|
||||||
|
git_repo_root: Option<PathBuf>,
|
||||||
|
initial_head_commit: Option<String>,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Lifecycle:** Create → Watch filesystem → Generate patches on changes → Broadcast to plugins → Shutdown
|
**Lifecycle:** Create → Watch filesystem → Generate patches on changes → Broadcast to plugins → Shutdown
|
||||||
|
|
||||||
**Message Queue:** Batches changes (~100ms window), cursor system for reconnect recovery
|
**Message Queue:** Batches changes (~200ms window), cursor system for reconnect recovery
|
||||||
|
|
||||||
**WebSocket loop:**
|
**WebSocket loop:**
|
||||||
```rust
|
```rust
|
||||||
|
|
@ -256,11 +283,16 @@ ServeSession = {
|
||||||
__status = "NotStarted", -- NotStarted → Connecting → Connected → Disconnected
|
__status = "NotStarted", -- NotStarted → Connecting → Connected → Disconnected
|
||||||
__apiContext = ApiContext,
|
__apiContext = ApiContext,
|
||||||
__twoWaySync = false,
|
__twoWaySync = false,
|
||||||
|
__syncSourceOnly = false,
|
||||||
|
__syncScriptsOnly = false,
|
||||||
__reconciler = Reconciler,
|
__reconciler = Reconciler,
|
||||||
__instanceMap = InstanceMap,
|
__instanceMap = InstanceMap,
|
||||||
__changeBatcher = ChangeBatcher,
|
__changeBatcher = ChangeBatcher,
|
||||||
__confirmingPatch = nil,
|
__confirmingPatch = nil,
|
||||||
__isConfirming = false, -- Prevents stacking dialogs, changes during review
|
__isConfirming = false, -- Prevents stacking dialogs, changes during review
|
||||||
|
__serverInfo = nil,
|
||||||
|
__applyingPatch = false,
|
||||||
|
__oneShotSyncDone = false,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -367,6 +399,8 @@ InstanceMap = {
|
||||||
pausedUpdateInstances = {}, -- Set of instances to ignore changes from
|
pausedUpdateInstances = {}, -- Set of instances to ignore changes from
|
||||||
instancesToSignal = {}, -- Instance → RBXScriptConnection(s)
|
instancesToSignal = {}, -- Instance → RBXScriptConnection(s)
|
||||||
onInstanceChanged = fn, -- Callback: (instance, propertyName) → void
|
onInstanceChanged = fn, -- Callback: (instance, propertyName) → void
|
||||||
|
onInstanceInserted = fn, -- Callback: when a new instance is inserted (deferred Ref resolution)
|
||||||
|
scriptsOnlyTempIds = {}, -- Temp IDs for scripts-only mode new script additions
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -421,6 +455,7 @@ ChangeBatcher = {
|
||||||
__pendingPropertyChanges = {}, -- Instance → { PropertyName → true }
|
__pendingPropertyChanges = {}, -- Instance → { PropertyName → true }
|
||||||
__pendingUnresolvedRefs = {}, -- targetInstance → [{sourceInstance, propertyName}]
|
__pendingUnresolvedRefs = {}, -- targetInstance → [{sourceInstance, propertyName}]
|
||||||
__syncSourceOnly = false, -- Only sync Source property
|
__syncSourceOnly = false, -- Only sync Source property
|
||||||
|
__syncScriptsOnly = false, -- Only sync LuaSourceContainer instances
|
||||||
__paused = false, -- Pause during confirmation
|
__paused = false, -- Pause during confirmation
|
||||||
__renderSteppedConnection = conn, -- Frame loop connection
|
__renderSteppedConnection = conn, -- Frame loop connection
|
||||||
__instanceMap = instanceMap,
|
__instanceMap = instanceMap,
|
||||||
|
|
@ -737,7 +772,7 @@ Only **Updated** items get defaults. Added/Removed items remain `nil`.
|
||||||
|
|
||||||
The plugin uses a recursive change-count scoring algorithm (implemented in `matching.lua`) to pair incoming virtual instances with existing Studio instances during hydration.
|
The plugin uses a recursive change-count scoring algorithm (implemented in `matching.lua`) to pair incoming virtual instances with existing Studio instances during hydration.
|
||||||
|
|
||||||
**Entry point:** `Matching.matchChildren(virtualChildren, studioChildren, virtualInstances)`
|
**Entry point:** `Matching.matchChildren(session, virtualChildren, studioChildren, virtualInstances, parentVirtualId?, parentStudioInstance?, _depth?)`
|
||||||
|
|
||||||
**Algorithm:**
|
**Algorithm:**
|
||||||
1. **Fast path:** Group by (Name, ClassName). 1:1 groups get instant-matched with zero scoring.
|
1. **Fast path:** Group by (Name, ClassName). 1:1 groups get instant-matched with zero scoring.
|
||||||
|
|
@ -781,7 +816,7 @@ RUST_LOG=rojo::web=debug,rojo::serve_session=debug atlas serve
|
||||||
Settings:set("logLevel", "Trace")
|
Settings:set("logLevel", "Trace")
|
||||||
```
|
```
|
||||||
|
|
||||||
Browser: `http://localhost:34872` → Network tab → WebSocket messages
|
Browser: `http://localhost:34873` → Network tab → WebSocket messages
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
|
|
||||||
|
|
@ -794,4 +829,33 @@ Browser: `http://localhost:34872` → Network tab → WebSocket messages
|
||||||
| `src/cli/serve.rs` - Live syncback handler | `plugin/src/Reconciler/` - Patch application |
|
| `src/cli/serve.rs` - Live syncback handler | `plugin/src/Reconciler/` - Patch application |
|
||||||
| `src/change_processor.rs` - FS writer | `plugin/src/Reconciler/matching.lua` - Matching |
|
| `src/change_processor.rs` - FS writer | `plugin/src/Reconciler/matching.lua` - Matching |
|
||||||
| `src/rojo_ref.rs` - Ref paths | `plugin/src/PatchTree.lua` - Default selections |
|
| `src/rojo_ref.rs` - Ref paths | `plugin/src/PatchTree.lua` - Default selections |
|
||||||
| `src/git.rs` - Git integration | `plugin/src/App/init.lua` - Live syncback UI |
|
| `src/git.rs` - Git integration (CLI-based) | `plugin/src/App/init.lua` - Live syncback UI |
|
||||||
|
| `src/web/mcp.rs` - MCP JSON-RPC | `plugin/src/McpStream.lua` - MCP stream client |
|
||||||
|
| `src/logging.rs` - File logging | `plugin/src/McpTools/` - Studio tool dispatch |
|
||||||
|
|
||||||
|
## MCP Integration
|
||||||
|
|
||||||
|
AI agent-facing JSON-RPC layer exposed during `atlas serve`. Agents call `POST /mcp`; the server handles some tools directly and dispatches others to the plugin over the MCP stream WebSocket.
|
||||||
|
|
||||||
|
**Architecture:**
|
||||||
|
```
|
||||||
|
AI Agent → POST /mcp (JSON-RPC) → Server (McpState) → ws://host:port/api/mcp/stream → Plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
**McpState** (`src/web/mcp.rs`): Shared state for agent-plugin communication. Uses a watch channel (`command_tx`/`command_rx`) for commands and a oneshot channel (`result_tx`) for results. Tracks `plugin_stream_connected` and `plugin_config`.
|
||||||
|
|
||||||
|
**MCP tools:**
|
||||||
|
|
||||||
|
| Tool | Handled by | Purpose |
|
||||||
|
|------|-----------|---------|
|
||||||
|
| `atlas_sync` | Server | Snapshot diff, sends sync command to plugin |
|
||||||
|
| `get_script` | Server | Resolves id/fsPath to script, returns Source |
|
||||||
|
| `syncback` | Plugin | Full Roblox → filesystem sync via `/api/mcp/syncback` |
|
||||||
|
| `run_code` | Plugin | Execute Luau code in Studio |
|
||||||
|
| `insert_model` | Plugin | Insert model from Creator Store |
|
||||||
|
| `get_console_output` | Plugin | Return console output |
|
||||||
|
| `get_studio_mode` | Plugin | Return Studio mode (Edit/Play) |
|
||||||
|
| `start_stop_play` | Plugin | Start/stop Play mode |
|
||||||
|
| `run_script_in_play_mode` | Plugin | Run code in a play session |
|
||||||
|
|
||||||
|
**Plugin side:** `McpStream.lua` connects to `ws://host:port/api/mcp/stream`, sends greeting with plugin config, handles incoming commands, and forwards results back. `McpTools/init.luau` dispatches Studio-side tools to individual handler files.
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,11 @@ Pure, stateless. Replaces forbidden filesystem characters with `_`.
|
||||||
const FORBIDDEN_CHARS: [char; 9] = ['<', '>', ':', '"', '/', '|', '?', '*', '\\'];
|
const FORBIDDEN_CHARS: [char; 9] = ['<', '>', ':', '"', '/', '|', '?', '*', '\\'];
|
||||||
const SLUGIFY_CHARS: [char; 10] = ['<', '>', ':', '"', '/', '|', '?', '*', '\\', '~'];
|
const SLUGIFY_CHARS: [char; 10] = ['<', '>', ':', '"', '/', '|', '?', '*', '\\', '~'];
|
||||||
const DANGEROUS_SUFFIXES: [&str; 8] = [".server", ".client", ".plugin", ".local", ".legacy", ".meta", ".model", ".project"];
|
const DANGEROUS_SUFFIXES: [&str; 8] = [".server", ".client", ".plugin", ".local", ".legacy", ".meta", ".model", ".project"];
|
||||||
const INVALID_WINDOWS_NAMES: [&str; 22] = ["CON", "PRN", "AUX", "NUL", "COM1".."COM9", "LPT1".."LPT9"];
|
const INVALID_WINDOWS_NAMES: [&str; 22] = [
|
||||||
|
"CON", "PRN", "AUX", "NUL",
|
||||||
|
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
||||||
|
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
||||||
|
];
|
||||||
```
|
```
|
||||||
|
|
||||||
**Behavior:**
|
**Behavior:**
|
||||||
|
|
@ -79,12 +83,13 @@ const INVALID_WINDOWS_NAMES: [&str; 22] = ["CON", "PRN", "AUX", "NUL", "COM1".."
|
||||||
- Strips leading/trailing spaces and dots
|
- Strips leading/trailing spaces and dots
|
||||||
- Neutralizes Windows reserved names (`CON` → `CON_`)
|
- Neutralizes Windows reserved names (`CON` → `CON_`)
|
||||||
- Neutralizes dangerous suffixes (`.server` etc.) that would be misinterpreted as middleware extensions
|
- Neutralizes dangerous suffixes (`.server` etc.) that would be misinterpreted as middleware extensions
|
||||||
|
- Neutralizes reserved name `"init"` (case-insensitive) → `"init_"` to avoid collision with init file convention
|
||||||
|
|
||||||
**Key invariant:** `slugify_name` is idempotent. Applying it twice produces the same result.
|
**Key invariant:** `slugify_name` is idempotent. Applying it twice produces the same result.
|
||||||
|
|
||||||
### name_needs_slugify
|
### name_needs_slugify
|
||||||
|
|
||||||
Returns `true` if the name requires slugification. Checks for: `SLUGIFY_CHARS`, control characters, leading/trailing spaces, trailing dots, dangerous suffixes (`.server` etc.), Windows reserved names (`CON` etc.), and empty string. Used to decide whether metadata `name` field is needed.
|
Returns `true` if the name requires slugification. Checks for: `SLUGIFY_CHARS`, control characters, leading/trailing spaces, trailing dots, dangerous suffixes (`.server` etc.), Windows reserved names (`CON` etc.), reserved name `"init"` (case-insensitive), and empty string. Used to decide whether metadata `name` field is needed.
|
||||||
|
|
||||||
### deduplicate_name_with_ext
|
### deduplicate_name_with_ext
|
||||||
|
|
||||||
|
|
@ -193,6 +198,7 @@ pub fn match_children(
|
||||||
new_dom: &WeakDom, old_dom: &WeakDom,
|
new_dom: &WeakDom, old_dom: &WeakDom,
|
||||||
new_hashes: Option<&HashMap<Ref, Hash>>,
|
new_hashes: Option<&HashMap<Ref, Hash>>,
|
||||||
old_hashes: Option<&HashMap<Ref, Hash>>,
|
old_hashes: Option<&HashMap<Ref, Hash>>,
|
||||||
|
session: &MatchingSession,
|
||||||
) -> MatchResult
|
) -> MatchResult
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -202,16 +208,21 @@ pub fn match_forward(
|
||||||
snapshot_children: Vec<InstanceSnapshot>,
|
snapshot_children: Vec<InstanceSnapshot>,
|
||||||
tree_children: &[Ref],
|
tree_children: &[Ref],
|
||||||
tree: &RojoTree,
|
tree: &RojoTree,
|
||||||
|
session: &MatchingSession,
|
||||||
) -> ForwardMatchResult
|
) -> ForwardMatchResult
|
||||||
```
|
```
|
||||||
|
|
||||||
**Lua plugin:**
|
**Lua plugin:**
|
||||||
```lua
|
```lua
|
||||||
function Matching.matchChildren(
|
function Matching.matchChildren(
|
||||||
virtualChildren: { string }, -- array of virtual instance IDs
|
session: MatchingSession,
|
||||||
studioChildren: { Instance }, -- array of Studio Instance objects
|
virtualChildren: { string },
|
||||||
virtualInstances: { [string]: any } -- map of virtual ID → virtual instance data
|
studioChildren: { Instance },
|
||||||
) -> { matched, unmatchedVirtual, unmatchedStudio }
|
virtualInstances: VirtualInstances,
|
||||||
|
parentVirtualId: string?,
|
||||||
|
parentStudioInstance: Instance?,
|
||||||
|
_depth: number?
|
||||||
|
) -> MatchResult
|
||||||
```
|
```
|
||||||
|
|
||||||
### Algorithm (All Three Implementations)
|
### Algorithm (All Three Implementations)
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,14 @@ alwaysApply: false
|
||||||
|
|
||||||
Built with Roact (React-like), handles WebSocket sync with server, patch application to Studio, and UI.
|
Built with Roact (React-like), handles WebSocket sync with server, patch application to Studio, and UI.
|
||||||
|
|
||||||
**Entry:** `plugin/src/init.server.lua` | **Protocol:** WebSocket v6 | **Must match server MAJOR.MINOR**
|
**Entry:** `plugin/src/init.server.lua` | **Protocol:** WebSocket v6 | **Must match server exact version**
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### 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. Also renders `SyncbackConfirm` floating dialog (StudioPluginGui with `isEphemeral=true`, not a StatusPage file). Contains `performSyncback()` and `SYNCBACK_SERVICES` constant.
|
- **init.lua** - Root component, manages connection state, auto-reconnect, playtest server auto-connect. Also renders `SyncbackConfirm` floating dialog (StudioPluginGui with `isEphemeral=true`, not a StatusPage file). Contains `performSyncback()` and `SYNCBACK_SERVICES` constant. Creates `McpStream` for MCP integration and handles `startMcpSync`, `handleMcpGetScript`, `collectAndPostSyncback` (to `/api/mcp/syncback`), dispatches tool calls via `McpTools.dispatch`. Note: sync reminder polling is currently disabled (returns immediately).
|
||||||
- **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
|
||||||
|
|
@ -25,9 +25,9 @@ Built with Roact (React-like), handles WebSocket sync with server, patch applica
|
||||||
- `Studio/` - Studio API wrappers (`StudioPluginAction`, `StudioPluginContext`, `StudioPluginGui`, `StudioToggleButton`, `StudioToolbarContext`, `StudioToolbar`)
|
- `Studio/` - Studio API wrappers (`StudioPluginAction`, `StudioPluginContext`, `StudioPluginGui`, `StudioToggleButton`, `StudioToolbarContext`, `StudioToolbar`)
|
||||||
- General: `Header`, `TextInput`, `TextButton`, `Checkbox`, `Dropdown`, `Tag`, `IconButton`, `Tooltip`, `TouchRipple`, `VirtualScroller`, `ScrollingFrame`, `Spinner`, `BorderedContainer`, `EditableImage`, `SlicedImage`, `ClassIcon`
|
- General: `Header`, `TextInput`, `TextButton`, `Checkbox`, `Dropdown`, `Tag`, `IconButton`, `Tooltip`, `TouchRipple`, `VirtualScroller`, `ScrollingFrame`, `Spinner`, `BorderedContainer`, `EditableImage`, `SlicedImage`, `ClassIcon`
|
||||||
- **StatusPages/** - Main UI pages
|
- **StatusPages/** - Main UI pages
|
||||||
- `NotConnected.lua` - Connection form (host/port input), "Syncback" button (Danger style)
|
- `NotConnected.lua` - Connection form (host/port input), "Sync" button (Primary style), "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. Accept button style=Primary/Neutral based on allSelected; text=Accept/OK based on hasChanges. Abort button only shown when hasChanges.
|
||||||
- `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`)
|
||||||
|
|
@ -39,8 +39,16 @@ Built with Roact (React-like), handles WebSocket sync with server, patch applica
|
||||||
- Protocol version checking
|
- Protocol version checking
|
||||||
- Confirmation flow with push/pull/ignore
|
- Confirmation flow with push/pull/ignore
|
||||||
- One-shot mode support
|
- One-shot mode support
|
||||||
|
- `ApiContext:getGitMetadata()` fetches git metadata from `GET /api/git-metadata` (separate from `/api/rojo`)
|
||||||
|
|
||||||
**3. Reconciler** (`Reconciler/`)
|
**3. MCP Integration** (`McpStream.lua`, `McpTools/`)
|
||||||
|
- **McpStream.lua** - WebSocket client to `ws://host:port/api/mcp/stream`. `RECONNECT_INTERVAL = 3` seconds. Sends greeting with plugin config (`twoWaySync`, `oneShotSync`, `confirmationBehavior`, `placeId`). Handles commands: `sync`, `getScript`, `syncback`, and passthrough tools. Forwards Roblox console output via `ConsoleOutput.onLogMessage`.
|
||||||
|
- **McpTools/** - Studio-side MCP tool dispatch
|
||||||
|
- `init.luau` - Dispatcher for: `run_code`, `insert_model`, `get_console_output`, `get_studio_mode`, `start_stop_play`, `run_script_in_play_mode`
|
||||||
|
- Tool handlers: `RunCode.luau`, `InsertModel.luau`, `GetConsoleOutput.luau`, `GetStudioMode.luau`, `StartStopPlay.luau`, `RunScriptInPlayMode.luau`
|
||||||
|
- `Utils/` - `ConsoleOutput.luau`, `StudioModeState.luau`, `GameStopUtil.luau`, `DeepCopy.luau`
|
||||||
|
|
||||||
|
**4. Reconciler** (`Reconciler/`)
|
||||||
- **applyPatch.lua** - Applies server patches to Studio tree
|
- **applyPatch.lua** - Applies server patches to Studio tree
|
||||||
- **reify.lua** - Creates instances from snapshots (parent last)
|
- **reify.lua** - Creates instances from snapshots (parent last)
|
||||||
- **hydrate.lua** - Updates InstanceMap with existing Studio instances
|
- **hydrate.lua** - Updates InstanceMap with existing Studio instances
|
||||||
|
|
@ -62,14 +70,14 @@ Built with Roact (React-like), handles WebSocket sync with server, patch applica
|
||||||
- **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)
|
||||||
|
|
||||||
**5. State Management**
|
**6. State Management**
|
||||||
- **Settings.lua** - Persistent plugin settings (prefixed `Atlas_` in plugin store)
|
- **Settings.lua** - Persistent plugin settings (prefixed `Atlas_` in plugin store)
|
||||||
- **InstanceMap.lua** - Bidirectional Server ID <-> Studio Instance mapping with change detection
|
- **InstanceMap.lua** - Bidirectional Server ID <-> Studio Instance mapping with change detection
|
||||||
- **PatchSet.lua** / **PatchTree.lua** - Patch representation and visualization tree (PatchTree.build accepts optional `gitMetadata` for smart default selections)
|
- **PatchSet.lua** / **PatchTree.lua** - Patch representation and visualization tree (PatchTree.build accepts optional `gitMetadata` for smart default selections)
|
||||||
- **Types.lua** - Type definitions and runtime validation
|
- **Types.lua** - Type definitions and runtime validation
|
||||||
- **ChangeMetadata.lua** - Change metadata tracking
|
- **ChangeMetadata.lua** - Change metadata tracking
|
||||||
|
|
||||||
**6. Utilities**
|
**7. Utilities**
|
||||||
- **Timer.lua** - Performance timing
|
- **Timer.lua** - Performance timing
|
||||||
- **DiffUtil.lua** - Diff utilities
|
- **DiffUtil.lua** - Diff utilities
|
||||||
- **Version.lua** - Version parsing and comparison
|
- **Version.lua** - Version parsing and comparison
|
||||||
|
|
@ -84,10 +92,10 @@ Built with Roact (React-like), handles WebSocket sync with server, patch applica
|
||||||
- **strict.lua** - Strict module wrapper
|
- **strict.lua** - Strict module wrapper
|
||||||
- **invariant.lua** - Assertion utilities
|
- **invariant.lua** - Assertion utilities
|
||||||
|
|
||||||
**7. Testing**
|
**8. Testing**
|
||||||
- Unit tests: `.spec.lua` files (TestEZ) throughout all modules
|
- Unit tests: `.spec.lua` files (TestEZ) throughout all modules
|
||||||
- Integration tests: `integration/` directory (`confirmBehavior`, `oneShotSync`, `syncflow`, `twoWaySync`)
|
- Integration tests: `integration/` directory (`confirmBehavior`, `oneShotSync`, `syncflow`, `twoWaySync`)
|
||||||
- Stress tests: `.stress.spec.lua` files (applyPatch, diff, reify, encodeInstance, PatchSet)
|
- Stress tests: `.stress.spec.lua` files (applyPatch, diff, reify, encodeInstance, PatchSet, InstanceMap)
|
||||||
- Chaos tests: `chaos/` directory (`stressPatterns.spec.lua`)
|
- Chaos tests: `chaos/` directory (`stressPatterns.spec.lua`)
|
||||||
- Test utilities: `testUtils/` (`MockApiContext`, `PatchGenerator`, `LargeTreeGenerator`)
|
- Test utilities: `testUtils/` (`MockApiContext`, `PatchGenerator`, `LargeTreeGenerator`)
|
||||||
- Test runner: `runTests.lua`
|
- Test runner: `runTests.lua`
|
||||||
|
|
@ -109,7 +117,8 @@ The plugin is built from `plugin.project.json` at the repo root:
|
||||||
"Fmt": { "$path": "plugin/fmt" },
|
"Fmt": { "$path": "plugin/fmt" },
|
||||||
"RbxDom": { "$path": "plugin/rbx_dom_lua" }
|
"RbxDom": { "$path": "plugin/rbx_dom_lua" }
|
||||||
},
|
},
|
||||||
"Version": { "$path": "plugin/Version.txt" }
|
"Version": { "$path": "plugin/Version.txt" },
|
||||||
|
"UploadDetails": { "$path": "plugin/UploadDetails.json" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -123,7 +132,7 @@ Vendored packages live under `plugin/Packages/`. Custom modules (`Log`, `Http`,
|
||||||
isDevBuild = false, -- Detected from ROJO_DEV_BUILD instance
|
isDevBuild = false, -- Detected from ROJO_DEV_BUILD instance
|
||||||
codename = "Epiphany",
|
codename = "Epiphany",
|
||||||
version = { major, minor, patch, metadata }, -- Parsed from Version.txt
|
version = { major, minor, patch, metadata }, -- Parsed from Version.txt
|
||||||
expectedServerVersionString = "7.7 or newer",
|
expectedServerVersionString = "8.5.0", -- Dynamic from Version.txt (e.g. "%d.%d.%d")
|
||||||
protocolVersion = 6, -- MUST match server
|
protocolVersion = 6, -- MUST match server
|
||||||
defaultHost = "localhost",
|
defaultHost = "localhost",
|
||||||
defaultPort = "34873",
|
defaultPort = "34873",
|
||||||
|
|
@ -170,41 +179,51 @@ Persistent settings stored with `Atlas_` prefix in Studio plugin store:
|
||||||
3. Plugin sends: `{type = "Subscribe"}`
|
3. Plugin sends: `{type = "Subscribe"}`
|
||||||
4. Server sends patches on filesystem changes (MessagePack encoded)
|
4. Server sends patches on filesystem changes (MessagePack encoded)
|
||||||
|
|
||||||
**Patch structure:**
|
**Packet structure (SocketPacket):**
|
||||||
```lua
|
```lua
|
||||||
{
|
{
|
||||||
type = "ApplyPatch",
|
|
||||||
sessionId = "uuid",
|
sessionId = "uuid",
|
||||||
patch = {
|
packetType = "messages",
|
||||||
removed = {"id-1", "id-2"},
|
body = {
|
||||||
added = {
|
messageCursor = 5,
|
||||||
["id-3"] = {
|
messages = {
|
||||||
id = "id-3", className = "Folder", name = "New",
|
{
|
||||||
properties = {}, metadata = { ignoreUnknownInstances = false },
|
removed = {"id-1", "id-2"},
|
||||||
children = {"child-1"},
|
added = {
|
||||||
|
["id-3"] = {
|
||||||
|
Id = "id-3", ClassName = "Folder", Name = "New",
|
||||||
|
Properties = {}, Metadata = { ignoreUnknownInstances = false },
|
||||||
|
Children = {"child-1"}, Parent = "parent-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updated = {
|
||||||
|
{ id = "id-4", changedName = "Renamed", changedProperties = { Source = "code" } },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
updated = {
|
|
||||||
{ id = "id-4", changedName = "Renamed", changedProperties = { Source = "code" } },
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note:** `added` Instance fields use **PascalCase**; `updated` InstanceUpdate fields use **camelCase**. Plugin handler: `["messages"] = function(messagesPacket)`, iterates `messagesPacket.messages`.
|
||||||
|
|
||||||
## ServeSession (`ServeSession.lua`)
|
## ServeSession (`ServeSession.lua`)
|
||||||
|
|
||||||
Manages the lifecycle of a connection to the server.
|
Manages the lifecycle of a connection to the server.
|
||||||
|
|
||||||
**Status flow:** `NotStarted` -> `Connecting` -> `Connected` -> `Disconnected`
|
**Status flow:** `NotStarted` -> `Connecting` -> `Connected` -> `Disconnected`
|
||||||
|
|
||||||
|
**Key fields:** `__syncSourceOnly`, `__syncScriptsOnly` (from serverInfo), `__serverInfo`, `__applyingPatch`, `__oneShotSyncDone`, `enableSyncFallback`
|
||||||
|
|
||||||
**Key methods:**
|
**Key methods:**
|
||||||
- `new(options)` - Creates session with `ApiContext` and `twoWaySync` flag
|
- `new(options)` - Creates session with `ApiContext` and `twoWaySync` flag
|
||||||
- `start()` - Connects, computes initial patch, shows confirmation
|
- `start()` - Connects, fetches git metadata via `apiContext:getGitMetadata()`, computes initial patch, shows confirmation
|
||||||
- `stop()` - Disconnects and cleans up
|
- `stop()` - Disconnects and cleans up
|
||||||
- `__onWebSocketMessage()` - Handles incoming patches
|
- `__onWebSocketMessage()` - Handles incoming patches (packetType "messages")
|
||||||
- `__applyPatch()` - Applies patches via Reconciler
|
- `__applyPatch()` - Applies patches via Reconciler
|
||||||
- `__confirmAndApplyInitialPatch()` - Handles user confirmation with push/pull/ignore
|
- `__confirmAndApplyInitialPatch()` - Handles user confirmation with push/pull/ignore
|
||||||
- `__computeInitialPatch()` - Computes catch-up patch
|
- `__computeInitialPatch()` - Computes catch-up patch
|
||||||
|
- `__connectScriptsOnlyWatchers()` - Connects `DescendantAdded` on visible services for scripts-only mode
|
||||||
- `hookPrecommit()` / `hookPostcommit()` - Lifecycle hooks
|
- `hookPrecommit()` / `hookPostcommit()` - Lifecycle hooks
|
||||||
|
|
||||||
**Confirmation flow:**
|
**Confirmation flow:**
|
||||||
|
|
@ -240,7 +259,7 @@ Manages the lifecycle of a connection to the server.
|
||||||
|
|
||||||
Pairs incoming virtual instances with existing Studio instances during hydration.
|
Pairs incoming virtual instances with existing Studio instances during hydration.
|
||||||
|
|
||||||
**Entry point:** `Matching.matchChildren(virtualChildren, studioChildren, virtualInstances)`
|
**Entry point:** `Matching.matchChildren(session, virtualChildren, studioChildren, virtualInstances, parentVirtualId?, parentStudioInstance?, _depth?)`
|
||||||
|
|
||||||
**Algorithm:**
|
**Algorithm:**
|
||||||
1. **Fast path:** Group by (Name, ClassName). 1:1 groups get instant-matched with zero scoring.
|
1. **Fast path:** Group by (Name, ClassName). 1:1 groups get instant-matched with zero scoring.
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@ alwaysApply: true
|
||||||
|-------|------|----------|-------------|
|
|-------|------|----------|-------------|
|
||||||
| `name` | string | Yes* | Instance name (*optional if `default.project.json5`) |
|
| `name` | string | Yes* | Instance name (*optional if `default.project.json5`) |
|
||||||
| `tree` | ProjectNode | Yes | Root instance tree structure |
|
| `tree` | ProjectNode | Yes | Root instance tree structure |
|
||||||
| `servePort` | number | No | Port for `atlas serve` (default: 34872) |
|
| `$schema` | string | No | JSON schema URL for editor validation (preserved during syncback roundtrip) |
|
||||||
|
| `servePort` | number | No | Port for `atlas serve` (default: 34873) |
|
||||||
| `serveAddress` | string | No | Address to bind (default: `"127.0.0.1"`) |
|
| `serveAddress` | string | No | Address to bind (default: `"127.0.0.1"`) |
|
||||||
| `servePlaceIds` | number[] | No | Only allow sync to these place IDs |
|
| `servePlaceIds` | number[] | No | Only allow sync to these place IDs |
|
||||||
| `blockedPlaceIds` | number[] | No | Block sync to these place IDs |
|
| `blockedPlaceIds` | number[] | No | Block sync to these place IDs |
|
||||||
|
|
@ -34,7 +35,7 @@ alwaysApply: true
|
||||||
"$schema": "https://raw.githubusercontent.com/UserGeneratedLLC/vscode-rojo/master/schemas/project.schema.json",
|
"$schema": "https://raw.githubusercontent.com/UserGeneratedLLC/vscode-rojo/master/schemas/project.schema.json",
|
||||||
"name": "MyProject",
|
"name": "MyProject",
|
||||||
"tree": { /* ProjectNode */ },
|
"tree": { /* ProjectNode */ },
|
||||||
"servePort": 34872,
|
"servePort": 34873,
|
||||||
"serveAddress": "0.0.0.0",
|
"serveAddress": "0.0.0.0",
|
||||||
"servePlaceIds": [123456],
|
"servePlaceIds": [123456],
|
||||||
"blockedPlaceIds": [111111],
|
"blockedPlaceIds": [111111],
|
||||||
|
|
@ -195,7 +196,7 @@ Configure `syncbackRules` object for Roblox → filesystem sync:
|
||||||
| `syncCurrentCamera` | bool | `false` | Whether to sync `Workspace.CurrentCamera` |
|
| `syncCurrentCamera` | bool | `false` | Whether to sync `Workspace.CurrentCamera` |
|
||||||
| `syncUnscriptable` | bool | `false` | Whether to sync unscriptable properties |
|
| `syncUnscriptable` | bool | `false` | Whether to sync unscriptable properties |
|
||||||
| `ignoreReferents` | bool | `false` | Whether to skip Ref properties (e.g., `Model.PrimaryPart`) |
|
| `ignoreReferents` | bool | `false` | Whether to skip Ref properties (e.g., `Model.PrimaryPart`) |
|
||||||
| `createIgnoreDirPaths` | bool | `false` | Whether `/**` in ignore paths also matches the directory itself |
|
| `createIgnoreDirPaths` | bool | `true` | Whether `/**` in ignore paths also matches the directory itself |
|
||||||
| `ignoreHiddenServices` | bool | — | Override root-level `ignoreHiddenServices` for syncback |
|
| `ignoreHiddenServices` | bool | — | Override root-level `ignoreHiddenServices` for syncback |
|
||||||
| `warnDuplicateNames` | bool | `false` | Warn on duplicate child names during syncback |
|
| `warnDuplicateNames` | bool | `false` | Warn on duplicate child names during syncback |
|
||||||
|
|
||||||
|
|
@ -249,10 +250,12 @@ Add metadata to file-based instances. Meta files use **camelCase keys** (no `$`
|
||||||
|
|
||||||
**Key differences from ProjectNode:** Meta uses `className`, `properties`, `attributes` (camelCase, no `$` prefix). Adjacent meta does NOT support `className`.
|
**Key differences from ProjectNode:** Meta uses `className`, `properties`, `attributes` (camelCase, no `$` prefix). Adjacent meta does NOT support `className`.
|
||||||
|
|
||||||
**`name` field (slugify support):** When an instance name contains forbidden filesystem characters (`: / ? * < > | " \`), syncback slugifies the filename (forbidden chars → `_`) and stores the real name in the `name` field. On forward sync, `name` overrides the filename-derived instance name. On collisions (e.g. `A/B` and `A_B` both slugify to `A_B`), `~2`, `~3` suffixes are appended and meta `name` fields distinguish them.
|
**`name` field (slugify support):** When an instance name contains forbidden filesystem characters (`: / ? * < > | " \`), or is the reserved name `"init"` (case-insensitive, slugified to `"init_"`), syncback slugifies the filename (forbidden chars → `_`) and stores the real name in the `name` field. On forward sync, `name` overrides the filename-derived instance name. On collisions (e.g. `A/B` and `A_B` both slugify to `A_B`), `~2`, `~3` suffixes are appended and meta `name` fields distinguish them.
|
||||||
|
|
||||||
**`Rojo_Ref_*` attributes:** Meta files may also contain `Rojo_Ref_PropertyName` attributes written by syncback/two-way sync for Ref property linking. These are managed automatically and should not be hand-edited.
|
**`Rojo_Ref_*` attributes:** Meta files may also contain `Rojo_Ref_PropertyName` attributes written by syncback/two-way sync for Ref property linking. These are managed automatically and should not be hand-edited.
|
||||||
|
|
||||||
|
Meta files also support a `$schema` field (preserved during syncback roundtrip).
|
||||||
|
|
||||||
**Meta files CAN'T:** Set `$path`, have children.
|
**Meta files CAN'T:** Set `$path`, have children.
|
||||||
|
|
||||||
## Model.json5 Format
|
## Model.json5 Format
|
||||||
|
|
@ -266,16 +269,17 @@ Inline instance definition. Like meta files, model files use **camelCase keys**
|
||||||
"id": "unique-id",
|
"id": "unique-id",
|
||||||
"properties": { "CanBeDropped": false },
|
"properties": { "CanBeDropped": false },
|
||||||
"attributes": { "Damage": 50 },
|
"attributes": { "Damage": 50 },
|
||||||
"children": {
|
"children": [
|
||||||
"Handle": {
|
{
|
||||||
|
"name": "Handle",
|
||||||
"className": "Part",
|
"className": "Part",
|
||||||
"properties": { "Size": [1, 1, 3] }
|
"properties": { "Size": [1, 1, 3] }
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note:** Children are nested under a `"children"` key (not flattened like in ProjectNode).
|
**Note:** `children` is an **array** of child objects (each with a `name` field), not a map/object keyed by name. This differs from ProjectNode where children are flattened as keys.
|
||||||
|
|
||||||
## Property Formats
|
## Property Formats
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ Transforms files → `InstanceSnapshot`. Main entry: `snapshot_from_vfs()`
|
||||||
| `*.lua` | `lua.rs` | ModuleScript | Legacy |
|
| `*.lua` | `lua.rs` | ModuleScript | Legacy |
|
||||||
| `*.server.lua` | `lua.rs` | Script (RunContext.Legacy) | Legacy |
|
| `*.server.lua` | `lua.rs` | Script (RunContext.Legacy) | Legacy |
|
||||||
| `*.client.lua` | `lua.rs` | LocalScript | Legacy |
|
| `*.client.lua` | `lua.rs` | LocalScript | Legacy |
|
||||||
|
| `*.plugin.lua` | `lua.rs` | Script (RunContext.Plugin) | Legacy |
|
||||||
| `*.rbxm` / `*.rbxmx` | `rbxm.rs` / `rbxmx.rs` | Binary/XML model tree | |
|
| `*.rbxm` / `*.rbxmx` | `rbxm.rs` / `rbxmx.rs` | Binary/XML model tree | |
|
||||||
| `*.model.json5` | `json_model.rs` | JSON instance | Preferred |
|
| `*.model.json5` | `json_model.rs` | JSON instance | Preferred |
|
||||||
| `*.model.json` | `json_model.rs` | JSON instance | Legacy |
|
| `*.model.json` | `json_model.rs` | JSON instance | Legacy |
|
||||||
|
|
@ -84,6 +85,10 @@ Instance names with forbidden filesystem chars (`<>:"/\|?*~`) are handled by:
|
||||||
- `upload.rs` - Upload to Roblox.com
|
- `upload.rs` - Upload to Roblox.com
|
||||||
- `sourcemap.rs` - Generate sourcemaps
|
- `sourcemap.rs` - Generate sourcemaps
|
||||||
- `syncback.rs` - Roblox → filesystem
|
- `syncback.rs` - Roblox → filesystem
|
||||||
|
- `clone.rs` - Create project from Roblox place (init + syncback --download)
|
||||||
|
- `completions.rs` - Shell completions (bash, zsh, fish, PowerShell, elvish) via `clap_complete`
|
||||||
|
- `cursor.rs` - Open project directory in Cursor IDE
|
||||||
|
- `studio.rs` - Open project in Roblox Studio via URL
|
||||||
- `init.rs` / `fmt_project.rs` / `doc.rs` / `plugin.rs`
|
- `init.rs` / `fmt_project.rs` / `doc.rs` / `plugin.rs`
|
||||||
|
|
||||||
### 4. Syncback (`src/syncback/`)
|
### 4. Syncback (`src/syncback/`)
|
||||||
|
|
@ -102,9 +107,11 @@ Reverse: Roblox file → filesystem
|
||||||
|
|
||||||
### 5. Web Server (`src/web/`)
|
### 5. Web Server (`src/web/`)
|
||||||
|
|
||||||
- `api.rs` - HTTP/WebSocket endpoints, including `POST /api/syncback` handler (`handle_api_syncback`)
|
- `api.rs` - HTTP/WebSocket endpoints, including `POST /api/syncback`, `GET /api/git-metadata`, MCP stream/syncback handlers
|
||||||
- `interface.rs` - Wire types: patches, `SyncbackRequest`, `ServiceChunk`, `SyncbackPayload`, `ServerExitReason`
|
- `interface.rs` - Wire types: patches, `SyncbackRequest`, `ServiceChunk`, `SyncbackPayload`, `ServerExitReason`, `GitMetadata`
|
||||||
- `mod.rs` - `SyncbackSignal` (Mutex + Notify pattern), `LiveServer` (uses `tokio::select!` to wait for connections or syncback signal)
|
- `mod.rs` - `SyncbackSignal` (Mutex + Notify pattern), `LiveServer` (uses `tokio::select!` to wait for connections or syncback signal)
|
||||||
|
- `mcp.rs` - MCP JSON-RPC endpoint (`POST /mcp`), `McpState`, tool definitions and handlers
|
||||||
|
- `mcp_docs/` - MCP tool description markdown files
|
||||||
- Protocol version enforcement
|
- Protocol version enforcement
|
||||||
|
|
||||||
### 6. Project System (`src/project.rs`)
|
### 6. Project System (`src/project.rs`)
|
||||||
|
|
@ -155,6 +162,9 @@ pub struct SyncbackRules {
|
||||||
sync_current_camera: Option<bool>, // Default: false
|
sync_current_camera: Option<bool>, // Default: false
|
||||||
sync_unscriptable: Option<bool>, // Default: false
|
sync_unscriptable: Option<bool>, // Default: false
|
||||||
ignore_referents: Option<bool>, // Skip Ref properties, default: false
|
ignore_referents: Option<bool>, // Skip Ref properties, default: false
|
||||||
|
create_ignore_dir_paths: Option<bool>, // Default: true
|
||||||
|
ignore_hidden_services: Option<bool>, // Override root-level ignoreHiddenServices
|
||||||
|
warn_duplicate_names: Option<bool>, // Warn on duplicate child names
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -221,26 +231,28 @@ Review: `cargo insta review`
|
||||||
|
|
||||||
## Web API
|
## Web API
|
||||||
|
|
||||||
|
**Default port:** 34873
|
||||||
|
|
||||||
**HTTP:**
|
**HTTP:**
|
||||||
- `GET /` - Browser UI
|
- `GET /` - Browser UI
|
||||||
- `GET /api/rojo` - Server info (version, project name, protocol)
|
- `GET /api/rojo` - Server info (version, project name, protocol)
|
||||||
- `POST /api/syncback` - Live syncback: full Roblox → filesystem sync (validates protocol, version, place ID)
|
- `GET /api/git-metadata` - Git metadata (changedIds, scriptCommittedHashes, newFileIds)
|
||||||
|
- `POST /api/syncback` - Live syncback: full Roblox → filesystem sync
|
||||||
|
- `POST /api/mcp/syncback` - MCP syncback (inline, returns stats)
|
||||||
|
- `POST /mcp` - MCP JSON-RPC endpoint
|
||||||
|
|
||||||
**WebSocket:** `ws://host:port/api/rojo`
|
**WebSocket:** `ws://host:port/api/socket/:cursor` (live patches), `ws://host:port/api/mcp/stream` (MCP)
|
||||||
|
|
||||||
**Protocol v6:**
|
**Protocol v6 (MessagePack):**
|
||||||
```json
|
```json
|
||||||
// Handshake
|
|
||||||
{"type": "ServerInfo", "protocolVersion": 6, "serverVersion": "7.7.0"}
|
|
||||||
|
|
||||||
// Patch
|
|
||||||
{
|
{
|
||||||
"type": "ApplyPatch",
|
|
||||||
"sessionId": "<uuid>",
|
"sessionId": "<uuid>",
|
||||||
"patch": {
|
"packetType": "messages",
|
||||||
"removed": [id1, id2],
|
"body": {
|
||||||
"added": {id3: {...}},
|
"messageCursor": 5,
|
||||||
"updated": {id4: {changedProperties: {...}}}
|
"messages": [
|
||||||
|
{ "removed": [], "added": {}, "updated": [] }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -278,7 +290,8 @@ pub struct SyncRule {
|
||||||
**Load project:**
|
**Load project:**
|
||||||
```rust
|
```rust
|
||||||
use librojo::Project;
|
use librojo::Project;
|
||||||
let project = Project::load_from_slice(path, contents)?;
|
let project = Project::load_fuzzy(&vfs, path)?;
|
||||||
|
// or: Project::load_exact(&vfs, path, fallback_name)?;
|
||||||
```
|
```
|
||||||
|
|
||||||
**Build snapshot:**
|
**Build snapshot:**
|
||||||
|
|
@ -314,11 +327,15 @@ to_writer(&mut buffer, &dom, dom.root())?;
|
||||||
|
|
||||||
## Key Dependencies
|
## Key Dependencies
|
||||||
|
|
||||||
**Core:** rbx_binary, rbx_xml, rbx_dom_weak, memofs (local crate)
|
**Core:** rbx_binary, rbx_xml, rbx_dom_weak, rbx_reflection, rbx_reflection_database, memofs (local crate)
|
||||||
**Async:** tokio, hyper, hyper-tungstenite
|
**Async:** tokio, hyper, hyper-util, http-body-util, hyper-tungstenite, reqwest, futures
|
||||||
**Serialization:** serde, json5, toml, yaml-rust2, csv
|
**Serialization:** serde, serde_json, json5, toml, yaml-rust2, csv, rmp-serde, serde_bytes, bincode
|
||||||
|
**Hashing:** blake3, sha1
|
||||||
|
**Logging:** log, tracing, tracing-appender, tracing-subscriber, time, dirs, flate2
|
||||||
|
**CLI:** clap (with derive, env), clap_complete
|
||||||
|
**Schema:** schemars
|
||||||
**Float precision:** lexical-write-float (for roundtrip-safe f32/f64 formatting in JSON)
|
**Float precision:** lexical-write-float (for roundtrip-safe f32/f64 formatting in JSON)
|
||||||
**Utilities:** clap, anyhow, thiserror, crossbeam-channel, rayon, globset
|
**Utilities:** anyhow, thiserror, crossbeam-channel, rayon, globset, uuid, walkdir, tempfile, pathdiff
|
||||||
|
|
||||||
## Module Organization
|
## Module Organization
|
||||||
|
|
||||||
|
|
@ -362,7 +379,7 @@ RUST_LOG=rojo=debug rojo build
|
||||||
|
|
||||||
**Watch events:** `RUST_LOG=memofs=trace`
|
**Watch events:** `RUST_LOG=memofs=trace`
|
||||||
|
|
||||||
**Protocol issues:** Browser dev tools on `http://localhost:34872`
|
**Protocol issues:** Browser dev tools on `http://localhost:34873`
|
||||||
|
|
||||||
## Common Pitfalls
|
## Common Pitfalls
|
||||||
|
|
||||||
|
|
@ -376,18 +393,27 @@ RUST_LOG=rojo=debug rojo build
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
cli/ - Commands
|
cli/ - Commands (serve, build, upload, syncback, sourcemap,
|
||||||
|
clone, completions, cursor, studio, init, fmt_project,
|
||||||
|
doc, plugin)
|
||||||
snapshot/ - Core system
|
snapshot/ - Core system
|
||||||
matching.rs - Forward sync matching algorithm
|
matching.rs - Forward sync matching algorithm
|
||||||
|
tree.rs - RojoTree (script_refs, known_paths(), path_to_ids)
|
||||||
snapshot_middleware/ - File handlers
|
snapshot_middleware/ - File handlers
|
||||||
syncback/ - Reverse sync
|
syncback/ - Reverse sync
|
||||||
matching.rs - Syncback matching algorithm
|
matching.rs - Syncback matching algorithm
|
||||||
dedup_suffix.rs - Dedup cleanup rules
|
dedup_suffix.rs - Dedup cleanup rules
|
||||||
meta.rs - Meta file helpers
|
meta.rs - Meta file helpers
|
||||||
web/ - HTTP/WebSocket (incl. SyncbackSignal, LiveServer)
|
web/ - HTTP/WebSocket server
|
||||||
interface.rs - Wire types (patches, syncback requests)
|
api.rs - HTTP/WebSocket endpoints
|
||||||
|
interface.rs - Wire types (patches, syncback requests, GitMetadata)
|
||||||
|
mcp.rs - MCP JSON-RPC, McpState, tool handlers
|
||||||
|
mcp_docs/ - MCP tool descriptions
|
||||||
change_processor.rs - Two-way sync filesystem writer
|
change_processor.rs - Two-way sync filesystem writer
|
||||||
git.rs - Git integration (changed files, SHA1 hashing, auto-staging)
|
git.rs - Git integration (CLI-based, changed files, SHA1, auto-staging)
|
||||||
|
logging.rs - Structured logging, file output to ~/.atlas/logs/
|
||||||
|
message_queue.rs - Patch message batching and cursor system
|
||||||
|
roblox_api.rs - Roblox auth, place download, experience name lookup
|
||||||
rojo_ref.rs - Ref path system (Rojo_Ref_*, RefPathIndex, Luau-style relative paths)
|
rojo_ref.rs - Ref path system (Rojo_Ref_*, RefPathIndex, Luau-style relative paths)
|
||||||
variant_eq.rs - Property value comparison
|
variant_eq.rs - Property value comparison
|
||||||
lib.rs - Public API
|
lib.rs - Public API
|
||||||
|
|
@ -397,6 +423,7 @@ crates/
|
||||||
memofs/ - Virtual filesystem
|
memofs/ - Virtual filesystem
|
||||||
|
|
||||||
tests/
|
tests/
|
||||||
|
end_to_end.rs - Integration test crate root
|
||||||
rojo_test/ - Test utilities
|
rojo_test/ - Test utilities
|
||||||
tests/ - Integration tests
|
tests/ - Integration tests
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -85,9 +85,17 @@ pub fn syncback_loop(
|
||||||
new_tree: WeakDom, // Roblox file contents
|
new_tree: WeakDom, // Roblox file contents
|
||||||
project: &Project, // Project configuration
|
project: &Project, // Project configuration
|
||||||
incremental: bool, // Preserve structure vs rebuild
|
incremental: bool, // Preserve structure vs rebuild
|
||||||
) -> anyhow::Result<FsSnapshot>
|
) -> anyhow::Result<SyncbackResult>
|
||||||
|
|
||||||
|
pub struct SyncbackResult {
|
||||||
|
pub fs_snapshot: FsSnapshot,
|
||||||
|
pub new_tree: WeakDom,
|
||||||
|
pub instance_paths: HashMap<Ref, String>,
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Also available: `syncback_loop_with_stats()` accepts `external_stats: Option<&SyncbackStats>` for callers that track stats externally.
|
||||||
|
|
||||||
## Syncback Modes
|
## Syncback Modes
|
||||||
|
|
||||||
### Incremental Mode (`incremental: true`)
|
### Incremental Mode (`incremental: true`)
|
||||||
|
|
@ -215,10 +223,11 @@ pub struct SyncbackRules {
|
||||||
```
|
```
|
||||||
|
|
||||||
**Property filtering** (`property_filter.rs`):
|
**Property filtering** (`property_filter.rs`):
|
||||||
- Filters out `Name`, `Parent` (always)
|
- Filters out `Name`, `Parent`, `Source`, `Value`, `Contents` via reflection DB (`DoesNotSerialize`), not explicit property lists
|
||||||
|
- Explicitly filters `Variant::Ref` and `Variant::UniqueId` by variant type
|
||||||
- Filters out properties in `ignore_properties`
|
- Filters out properties in `ignore_properties`
|
||||||
- Filters out unscriptable properties if `sync_unscriptable: false`
|
- Filters out unscriptable properties if `sync_unscriptable: false`
|
||||||
- Filters out middleware-specific properties (e.g., `Script.Source` when writing to `.luau` file)
|
- Middleware-specific filtering (e.g., `Script.Source` for `.luau` files) is handled in `snapshot.rs` via `filter_out_property`, not in `property_filter.rs`
|
||||||
|
|
||||||
### Hidden Services
|
### Hidden Services
|
||||||
|
|
||||||
|
|
@ -296,17 +305,14 @@ pub struct SyncbackSnapshot<'sync> {
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Join child to current path
|
// Join child to current path (returns (Self, needs_meta_name, dedup_key))
|
||||||
snapshot.with_joined_path(new_ref, old_ref)?
|
snapshot.with_joined_path(new_ref, old_ref, &taken_names)?
|
||||||
// Result: parent_path/ChildName.luau
|
|
||||||
|
|
||||||
// Use base path
|
// Use base path (same return type)
|
||||||
snapshot.with_base_path(&base, new_ref, old_ref)?
|
snapshot.with_base_path(&base, new_ref, old_ref, &taken_names)?
|
||||||
// Result: base/ChildName.luau
|
|
||||||
|
|
||||||
// Explicit path
|
// Explicit path
|
||||||
snapshot.with_new_path(path, new_ref, old_ref)
|
snapshot.with_new_path(path, new_ref, old_ref)
|
||||||
// Result: path (exactly as provided)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Filename generation** (`file_names.rs`):
|
**Filename generation** (`file_names.rs`):
|
||||||
|
|
@ -366,7 +372,7 @@ fs_snapshot.merge(other) // Combine snapshots
|
||||||
|
|
||||||
// Apply to filesystem
|
// Apply to filesystem
|
||||||
fs_snapshot.write_to_vfs(base_path, vfs)?;
|
fs_snapshot.write_to_vfs(base_path, vfs)?;
|
||||||
fs_snapshot.write_to_vfs_parallel(base_path, vfs)?; // Parallel writes (see below)
|
fs_snapshot.write_to_vfs_parallel(base_path, vfs, git_cache)?; // Parallel writes; git_cache: Option<&GitIndexCache> skips unchanged files
|
||||||
|
|
||||||
// Post-process ref paths
|
// Post-process ref paths
|
||||||
fs_snapshot.fix_ref_paths(&substitutions); // Replace placeholder paths with final relative paths
|
fs_snapshot.fix_ref_paths(&substitutions); // Replace placeholder paths with final relative paths
|
||||||
|
|
@ -416,7 +422,7 @@ Forward sync also reads `Rojo_Target_*` attributes and resolves via `get_specifi
|
||||||
- `collect_referents(dom, paths, final_paths)` - Finds all Ref properties, stores as (source, target_path) pairs
|
- `collect_referents(dom, paths, final_paths)` - Finds all Ref properties, stores as (source, target_path) pairs
|
||||||
- `tentative_fs_path(dom, ref)` - Computes pre-dedup path: slugified name + extension based on class/children
|
- `tentative_fs_path(dom, ref)` - Computes pre-dedup path: slugified name + extension based on class/children
|
||||||
- `tentative_fs_path_public()` - Public wrapper used in post-processing
|
- `tentative_fs_path_public()` - Public wrapper used in post-processing
|
||||||
- `link_referents(dom, referents)` - Writes `Rojo_Ref_*` attributes to instances
|
- `link_referents(links: RefLinks, dom: &mut WeakDom)` - Writes `Rojo_Ref_*` attributes to instances
|
||||||
|
|
||||||
**Ref path post-processing (in `mod.rs`):**
|
**Ref path post-processing (in `mod.rs`):**
|
||||||
|
|
||||||
|
|
@ -577,6 +583,22 @@ diff place.rbxl rebuilt.rbxl
|
||||||
- Globally optimal matching (Hungarian algorithm) for large ambiguous groups
|
- Globally optimal matching (Hungarian algorithm) for large ambiguous groups
|
||||||
- ~~Matching cache (parent_ref -> match_assignments) for avoiding redundant scoring~~ (session-level caching now implemented via `MatchingSession`; per-parent invalidation cache still deferred)
|
- ~~Matching cache (parent_ref -> match_assignments) for avoiding redundant scoring~~ (session-level caching now implemented via `MatchingSession`; per-parent invalidation cache still deferred)
|
||||||
|
|
||||||
|
## GitIndexCache Integration
|
||||||
|
|
||||||
|
`write_to_vfs_parallel()` accepts `git_cache: Option<&GitIndexCache>`. When provided (by `src/cli/syncback.rs` and `src/cli/serve.rs`), files whose SHA1 blob hash matches the git index are skipped. `GitIndexCache` is created via `GitIndexCache::new(project_root)` which runs `git ls-files --stage`.
|
||||||
|
|
||||||
|
## $schema Roundtrip
|
||||||
|
|
||||||
|
`.project.json5`, `.meta.json5`, and `.model.json5` files preserve a `$schema` field during syncback. Implemented via `schema: Option<String>` on `Project`, `AdjacentMetadata`, `DirectoryMetadata`, and `JsonModel` structs. Old instance metadata carries the schema through the pipeline.
|
||||||
|
|
||||||
|
## Reserved Name: `init`
|
||||||
|
|
||||||
|
`slugify_name()` appends `_` to `"init"` (case-insensitive): `"init"` -> `"init_"`. `name_needs_slugify()` returns `true` for `"init"`. This prevents collision with the init file convention (`init.luau`, `init.server.luau`, etc.).
|
||||||
|
|
||||||
|
## MCP Syncback
|
||||||
|
|
||||||
|
`POST /api/mcp/syncback` runs syncback inline and returns stats with a `syncback_complete` status. Unlike `/api/syncback` which restarts the serve loop, the MCP variant completes without server restart.
|
||||||
|
|
||||||
## Key Files to Reference
|
## Key Files to Reference
|
||||||
|
|
||||||
- `src/syncback/mod.rs` - Main orchestration loop, ref path post-processing
|
- `src/syncback/mod.rs` - Main orchestration loop, ref path post-processing
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,11 @@ Atlas bridges filesystem and Roblox Studio, enabling professional development to
|
||||||
**Core capabilities:**
|
**Core capabilities:**
|
||||||
- Filesystem-based development with live sync to Studio
|
- Filesystem-based development with live sync to Studio
|
||||||
- Build/upload rbxl/rbxm files from command line
|
- Build/upload rbxl/rbxm files from command line
|
||||||
- Syncback: pull instances from Roblox → filesystem (7.7.0+)
|
- Syncback: pull instances from Roblox → filesystem
|
||||||
- Two-way sync (plugin-based): live bidirectional sync during `atlas serve` sessions
|
- Two-way sync (plugin-based): live bidirectional sync during `atlas serve` sessions
|
||||||
- Live syncback: one-click full sync from Studio to filesystem during `atlas serve` (no WebSocket connection required)
|
- Live syncback: one-click full sync from Studio to filesystem during `atlas serve` (no WebSocket connection required)
|
||||||
|
- MCP integration: AI agent-facing JSON-RPC layer for programmatic sync, script access, and Studio tool dispatch
|
||||||
|
- Scripts-only mode (`syncScriptsOnly`): sync only script instances, ignore everything else
|
||||||
|
|
||||||
### Two-Way Sync Strategy
|
### Two-Way Sync Strategy
|
||||||
|
|
||||||
|
|
@ -49,7 +51,7 @@ Atlas bridges filesystem and Roblox Studio, enabling professional development to
|
||||||
|
|
||||||
### Three Components
|
### Three Components
|
||||||
|
|
||||||
1. **Server/CLI (Rust)** - `src/`, commands: serve, build, upload, syncback, sourcemap
|
1. **Server/CLI (Rust)** - `src/`, commands: serve, build, upload, syncback, sourcemap, clone, completions, cursor, studio
|
||||||
2. **Studio Plugin (Lua)** - `plugin/`, Roact UI, WebSocket sync
|
2. **Studio Plugin (Lua)** - `plugin/`, Roact UI, WebSocket sync
|
||||||
3. **rbx-dom (submodule)** - `rbx-dom/`, Roblox format libraries (binary/XML, types, reflection)
|
3. **rbx-dom (submodule)** - `rbx-dom/`, Roblox format libraries (binary/XML, types, reflection)
|
||||||
|
|
||||||
|
|
@ -314,7 +316,9 @@ Reconciler.applyPatch(instanceMap, patch)
|
||||||
|
|
||||||
```
|
```
|
||||||
src/ - Rust server/CLI
|
src/ - Rust server/CLI
|
||||||
cli/ - Command implementations
|
cli/ - Command implementations (serve, build, upload, syncback,
|
||||||
|
sourcemap, clone, completions, cursor, studio, init,
|
||||||
|
fmt_project, doc, plugin)
|
||||||
snapshot/ - Core snapshot system
|
snapshot/ - Core snapshot system
|
||||||
matching.rs - Forward sync matching algorithm
|
matching.rs - Forward sync matching algorithm
|
||||||
snapshot_middleware/ - File type handlers
|
snapshot_middleware/ - File type handlers
|
||||||
|
|
@ -323,9 +327,14 @@ src/ - Rust server/CLI
|
||||||
dedup_suffix.rs - Dedup cleanup rules
|
dedup_suffix.rs - Dedup cleanup rules
|
||||||
meta.rs - Meta file read/write helpers
|
meta.rs - Meta file read/write helpers
|
||||||
web/ - HTTP/WebSocket server
|
web/ - HTTP/WebSocket server
|
||||||
|
api.rs - HTTP/WebSocket endpoints
|
||||||
interface.rs - Wire types (patches, syncback requests)
|
interface.rs - Wire types (patches, syncback requests)
|
||||||
|
mcp.rs - MCP JSON-RPC endpoint, McpState, tool handlers
|
||||||
|
mcp_docs/ - MCP tool description files
|
||||||
change_processor.rs - Two-way sync filesystem writer
|
change_processor.rs - Two-way sync filesystem writer
|
||||||
git.rs - Git integration (changed files, SHA1 hashing, auto-staging)
|
git.rs - Git integration (CLI-based, changed files, SHA1, auto-staging)
|
||||||
|
logging.rs - Structured logging, file output to ~/.atlas/logs/, rotation
|
||||||
|
roblox_api.rs - Roblox auth, place download, experience name lookup
|
||||||
rojo_ref.rs - Ref path system (Rojo_Ref_*, RefPathIndex)
|
rojo_ref.rs - Ref path system (Rojo_Ref_*, RefPathIndex)
|
||||||
variant_eq.rs - Property value comparison
|
variant_eq.rs - Property value comparison
|
||||||
|
|
||||||
|
|
@ -336,6 +345,8 @@ plugin/src/ - Lua Studio plugin
|
||||||
trueEquals.lua - Shared value equality module
|
trueEquals.lua - Shared value equality module
|
||||||
ChangeBatcher/ - Two-way sync patch creation
|
ChangeBatcher/ - Two-way sync patch creation
|
||||||
encodeService.lua - Service-level property encoding for live syncback
|
encodeService.lua - Service-level property encoding for live syncback
|
||||||
|
McpStream.lua - MCP WebSocket client (ws://host:port/api/mcp/stream)
|
||||||
|
McpTools/ - Studio-side MCP tool dispatch (run_code, insert_model, etc.)
|
||||||
Config.lua - Version and protocol config
|
Config.lua - Version and protocol config
|
||||||
PatchTree.lua - Patch visualization tree (git-based default selections)
|
PatchTree.lua - Patch visualization tree (git-based default selections)
|
||||||
SHA1.luau - SHA1 hash for git blob format comparison
|
SHA1.luau - SHA1 hash for git blob format comparison
|
||||||
|
|
@ -368,7 +379,9 @@ rojo-test/ - Test fixtures and utilities
|
||||||
## Version Information
|
## Version Information
|
||||||
|
|
||||||
- **Protocol version:** 6 (`plugin/src/Config.lua`)
|
- **Protocol version:** 6 (`plugin/src/Config.lua`)
|
||||||
- **Server/Plugin must match:** MAJOR.MINOR (e.g., 7.7.x works with 7.7.y)
|
- **Version matching:** Plugin WebSocket connection requires **exact version match** (`ApiContext.lua`). Live syncback validation uses **MAJOR.MINOR** match (`api.rs`).
|
||||||
|
- **Current version:** 8.5.0
|
||||||
|
- **Default serve port:** 34873
|
||||||
- **MSRV:** Rust 1.88
|
- **MSRV:** Rust 1.88
|
||||||
- **License:** MPL-2.0 (rbx-dom uses MIT)
|
- **License:** MPL-2.0 (rbx-dom uses MIT)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue