Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit61a33237e0

README.md

world_vcs

Pure-Luau wrapper around the __spacetime_vcs.* FFI primitives. Composes the low-level stage / commit / push reducer chain into the headline world.add / world.commit / world.push / world.diff / etc. API. The prelude grafts these onto the engine's world namespace at boot via M.installInto(world) — callers use the grafted methods on world.*, they don't require this module directly.

This module is the ONLY place where the public world.* VCS API is defined. Future context-gating (only editor-scope VMs may invoke world.commit, components are denied, etc.) lives here as Luau-side checks. The Rust-side primitives stay one-to-one with module reducers and don't carry gating logic.

Exports

  • M.installInto(world: WorldNamespace) — graft the entire VCS surface (add, add_all, unstage, discard, commit, push, head, status, log, show, diff, affirm, trash, restore, stash, stashes, stashPop, stashDrop, reset, list, installLibrary, updateLibrary, installedLibraries, checkUpdates, reviewUpdate, update, uninstallLibrary, installAsset, uninstall, removeUnusedDeps) onto the supplied namespace.

After install, the following are callable on world.*:

  • world.add(paths: string | { string }): stage one path's manifest row, or each of a list of paths (asset-aware: expands to the asset family). A list stages every row its paths select once, so several files of one asset stage that asset's family once. Staging is advisory: a path another staging area holds is taken out of that area, whichever account opened it and whether or not its session is open, and named in the report's released (stage, account, same_account, session, live); a take from another account's area warns, naming that account, the area and its session. A staged path a caller of another account is writing now is named in written and warned about. The report is { staged, carried, released, written }.
  • world.add_all(opts?): stage every dirty manifest row, whoever wrote it, each with its asset family; conflicted rows stay out, and the call returns them. Staging is advisory: a path another staging area holds is taken from that area, whichever account opened it, and a row a caller of another account is writing is staged like any other. The calling account's own agents and engines never warn each other; a take from another account's area, and a staged path another account's open session is writing, warn once per area or caller. A second return value reports the call: staged (the paths it added), held (each conflicted path with its reason), released (each path taken from another staging area, with that area as stage, its row id as stage_row_id, its account, same_account, its session and whether that session is live) and written (each staged path a caller of another account is writing now, with by and account). Its cost follows the size of the dirty set: a fixed number of working-tree reads, and its stages and releases sent in batches.
  • world.unstage(path: string) — remove a path from the staging area. A path the calling area does not hold that another staging area does is taken out of that area, whichever account opened it; the call returns { released }.
  • world.stageVersion(rev, path) — git reset <rev> -- <path>: the staging area's entry for the path becomes the revision's version, and a version the branch head holds (HEAD itself) unstages the path; the working tree stays.
  • world.discard() — drop the implicit stage without committing.
  • world.commit(message: string) -> string — open-or-resume stage, set message, materialise commit. Returns the new commit id (ULID).
  • world.push(commitId: string?, opts: StageOpts?) -> (string?, string) — push one or all unpushed commits to ZeroMind. opts.stage names the caller's own staging area. The pre-push content gate refuses over every asset the push publishes that falls short of its type's contract, whichever area staged it, and names beside each one the other area holding it, when one does. Returns the pushed ZM commit id and the verdict "published", or the verdict "already-published" when the remote already carries what the call would have published — with the commit's existing ZM id for the single-commit form, and a nil id for the whole-stack form.
  • world.head() -> string? — current branch HEAD commit id, or nil.
  • world.vcsStatus(opts?) -> StatusResult — dirty + staged + untracked entries, and ignored_summary: the ignored rows counted by kind, one entry per scene autosave overlay (kind = "overlay", root its scene_dirty folder) and per .zmignore / .gitignore line that matches rows (kind = "rule", file, rule), each with its count. { ignored = true } lists the ignored rows themselves in ignored, which is empty otherwise. A file whose bytes are the head's and whose reference table is not (a library pin moved, or an asset it references changed) is listed with reason = "references moved", staged like any change, and committed with what moved it: a commit holding that change records each such file it does not stage at the bytes the branch head holds, with the pins that change moved advanced (a staged entry with references_only = true), so another caller's unstaged edit of the file stays out of the commit. world.commit refuses a commit holding a file whose table pins a version the commit leaves out (a changed asset or pin marker left unstaged), with a dep.stale_references entry per file. (Named vcsStatus, not status: world.status() is the runtime-snapshot accessor from world_status.module.)
  • world.log(opts: LogOpts?) -> { CommitRow } — git log: every commit reachable from opts.rev (default HEAD) through both parents, newest first; opts.path keeps the commits that touched one file.
  • world.revParse(rev: string) -> string — git rev-parse: the commit HEAD, a branch, origin/<branch>, a commit id or a unique prefix names, with ~N / ^ / ^2 after it.
  • world.show(...) -> any — git show-style: commit metadata + diff, or path bytes via <rev>:/path.
  • world.diff(...) -> any — git diff-style: working / staged / revision-vs-revision, with --stat / --name-only. Working-tree files a .zmignore / .gitignore rule matches are left out unless --ignored is passed; staged files and paths named after -- are diffed whatever rule matches them.
  • world.diffUpstream(root: string) -> { UpdateFile } — your changes to an installed asset against the version it was installed from.
  • world.createBranch(name, fromCommit?), world.branches(), world.remoteBranches(), world.deleteBranch(name, { force? }), world.renameBranch(old, new, { force? }) — git branch and its -r, -d / -D, -m / -M. A rename that fails part-way is undone, leaving the branches as they were.
  • world.checkout(branch: string) -> string — git checkout <branch>: the caller's uncommitted changes and staged entries come along, or the switch refuses with git's message, moving nothing. world.checkoutSide(path, "ours" | "theirs") takes one side of an unmerged path; world.checkoutPaths(rev, paths, opts?) writes and stages paths as a revision holds them, each as the asset the revision records (its guid, bytes and kind); a folder brings back every file the revision holds beneath it, and { stage = false } leaves the staging area as it was.
  • world.merge(source: string, opts: MergeOpts?) -> MergeResult — git merge into the session's branch: a fast-forward when it can (noFf / ffOnly), else a merge commit; a local change on a path the merge rewrites refuses it. world.mergeAbort() takes back what a conflicted merge wrote.
  • world.affirm(token: string) — consume an affirmation token returned by a destructive op.
  • world.trash() -> { TrashRow } — list trash entries.
  • world.restore(handle: any) — restore a trash entry by row_id.
  • world.trashEmpty() -> number — delete the trash entries your own operations put there, for good, in one transaction; other callers' entries stay.
  • world.stash(label: string?) -> StashPushResult — git stash push: save the caller's dirty + staged state into a stash and take those paths back to their committed version. Binary and large content is saved by its stored blob, so applying the stash brings it back byte-identical. A path no commit holds keeps its working content in the tree, as git leaves an untracked file, and is listed under kept, with its reason in reasons.
  • world.stashes() -> { StashRow } — list every stash in the world.
  • world.stashApply(ref?) / world.stashPop(ref?) / world.stashDrop(ref?) — git stash apply | pop | drop, defaulting to the caller's newest stash; ref is a row id or stash@{n}. A drop snapshots the stash to trash.
  • world.reset(target: string, opts: ResetOpts?) -> ResetResult — git reset --soft | --mixed | --hard to any revision; the commits the head leaves behind become trash entries. hard takes the working tree to the target: every change of the caller's account, untracked files, uncommitted installs and a scene's autosave overlay included, goes to trash first, and a write this session owes the world that the world is not taking is given up (world.dropOwedWrites); another account's local changes stay and are listed in kept.
  • world.list() -> { WorldEntry } — every world the authenticated user has access to.
  • world.installLibrary(opts: InstallLibraryOpts) -> InstallLibraryResult — declarative cross-world dependency marker.
  • world.updateLibrary(name: string, ref: string?) -> UpdateLibraryResult — move an installed library (@builtin included) to the commit ref (default main) resolves to in the library's world: rewrites the marker, waits until the engine holds the library at that commit (an @builtin pin table is read, another library is mounted), and derives again the reference tables of the world's own sources that name it; a ref resolving to the commit already pinned changes nothing. A commit that does not hold an asset the world's own sources name is refused before the marker changes: the error names each such asset by the literal its sites wrote and its guid, with every source and line naming it (twenty listed, then a count), and the pin stays where it was. It refuses while play is running or in a runtime-profile session, and one move per library runs at a time. Returns { name, from, to, changed, rederived, broken, kept, conflicts }; a pin already at that commit is left untouched (changed = false). The world's edits of the library's files stay at their paths through the move: kept lists the ones the new commit left as they were, conflicts the ones it changed or removed too, whose new version is for the author to merge in or take.
  • world.checkUpdates() -> ({ UpdateReport }, { UpdateSkip }) — the units with an update available, read-only. A library is one unit: its marker's requested_ref (default main) resolves in the library's world to a commit other than the marker's resolved_commit ({ kind = "library", library, from, to }; apply with world.updateLibrary(name)). An installed asset is one unit per install root (its reference table marks it): its recorded version differs from the newest version of the asset at its origin, one lookup per root ({ kind = "asset", root_path, root_guid, display_name, from, to }; apply with world.update(<root path>) (zeromind.update), which installs the root's new closure with each dependency at the version the new root records and removes dependency versions no install records any more). A dependency is never compared or reported, assets from a library's world belong to that library's unit, and a pulled row no table marks as a root is not reported. The second value lists { name, reason } for the units whose origin could not be reached. Opening a world in an editor raises one notice with the same list. world.pullAsset aimed at a dependency of an install answers <path> is a dependency of the install <name> (<root path>); update the root.
  • world.reviewUpdate(unit: string, opts: ReviewUpdateOpts?) -> UpdateReview — what updating one unit changes, writing nothing: from (what the world holds) and to (what the origin holds at opts.ref, default the ref world.checkUpdates compares it with), then commits, the commits of the origin world in between, newest first, each { id, message, author, date } (more is true past 200), then the files; modified names up front each file holding an edit of the world's own that the update touches, with merges or conflicts, and by, the session that wrote the uncommitted change: a file is edited when it is not an installed file holding the bytes it installed (an install nobody edited is never in modified, committed or not). An installed asset (its root path; a path inside the install stands for the root) lists the commits that change the install back to the one that moved the root from the installed version, and files: each file the update adds, changes or removes, { path, change, diff, hunks, local_edit }, local_edit being none, merges or conflicts for a file the world edited since it was installed. A library (@<name>, the @ optional) lists its commits from the pin to the new commit and counts the library files that change and the world's files whose references move with the pin; moving (the world's files whose references move) is always present; opts.files = true adds files, the list behind the counts; the moving files are read from the engine's reverse reference index. opts.commits = false reads no commits and opts.files = false leaves an install's files and diffs out, so { commits = false, files = false } is the check before an update: modified and the counts alone. A file listed in status only because its reference table moved (references moved, install recorded) holds no change of its own and is never in modified. An install's commit list stops at the commit whose parent holds the root at the version the install recorded, read from the root's closure at that commit, the root alone (zeromind.world op closure, maxDepth = 0), or at the commit that adds the root's path; when a version cannot be read the list stops there, more is true and commits_error says why. The origin's log, commits and diffs are read through the trusted ZeroMind bridge (zeromind.world ops commits, commit, diff).
  • world.pull(branch: string?, opts: PullOpts?) -> PullResult — git pull: fetch origin/<branch> (default the session branch) and merge it into the session branch: a fast-forward when the session branch is strictly behind, else a two-parent merge commit with markers on conflict; nothing when the session branch already holds the fetched commit, read before anything moves so a tree with uncommitted changes is not refused for it: for the session branch's own upstream the fetch's behind count answers that, for another branch the session branch's history does. opts.ffOnly refuses anything but a fast-forward, opts.noFf always makes a merge commit.
  • world.update(units: (string | { string })?, opts: UpdateOpts?) -> UpdateResult — apply updates: no units updates every unit world.checkUpdates lists, a name or a list of names (library names and install root paths, mixed) updates those, to opts.ref when given. A library moves its pin (world.updateLibrary), an installed asset pulls its new closure (world.pullAsset), one unit at a time; a unit that fails is reported in its entry with the reason and the rest still run. An install's pull refuses while a row it writes is conflicted (a row under its root, or the row a closure entry lands in), and runs over conflicts in every other row. Nothing is staged, committed or pushed: the updated files wait in world.status(). Returns { results, to_commit, skipped, warnings, note }: one { unit, kind, from, to, ok, error?, merged?, conflicted?, added?, pruned?, moving? } per unit, every path the updates changed or moved, the units the check could not read, one { unit, path, outcome = "merged" | "conflicted" } per file holding uncommitted changes that an update touched, and a note that nothing was committed.
  • world.uninstallLibrary(name: string) -> string — delete the library marker file.
  • world.uninstall(path: string, opts: { dryRun: boolean? }?) -> UninstallReport — remove the install at path with the dependencies under /zero/source/deps/ it placed or used that nothing else uses; a dependency something else uses stays (kept, with its users), and a file this world changed stays as a file of the world (kept_modified). Commits nothing. Lives in dependencies.module.
  • world.removeUnusedDeps(opts: { dryRun: boolean?, only: (string | { string })? }?) -> RemoveUnusedReport — list (dryRun) or remove the dependencies under /zero/source/deps/ nothing in the world uses, by the engine's reverse index of reference tables and the install records. Commits nothing. dependencies.module also posts the notice naming them whenever the set changes; the notice writes nothing to the world.
  • world.installedLibraries() -> { LibraryMarker } — the libraries this world has installed, one row per marker under /zero/source/libs, sorted by name: { name, world_guid, requested_ref?, resolved_commit }.
  • world.installAsset(opts: InstallAssetOpts) -> InstallAssetResult — imperative content pull from another world.
  • world.assetInstallable(opts: AssetInstallableOpts) -> AssetInstallableReport — whether one published asset resolves a closure of its own, as a verdict.
  • world.installedAssets() -> { InstalledAsset } — every asset this world carries from ZeroMind, keyed by the published guid it was pulled from.

Types:

  • WorldNamespace = { [string]: any } — target table for installInto.
  • LogOpts = { limit: number? }
  • CommitRow = { commit_id: string, parent_commit_id: string?, hivemind_pushed: boolean?, message: string?, created_at: number? }
  • StatusEntry = { path: string, dirtied_by: string?, dirtied_by_caller: string?, writer: string?, dirty_since_micros: number?, staged_action: string? }: dirtied_by is the identity the row was written under, which every session of one account shares; dirtied_by_caller is the caller key of the call that wrote it (client:<session> / actor:<sub>, and client:<session>@<engine seat> for an agent an engine hosts in its dock, whose session id every engine repeats; a row keyed by a bare client:ide-slot-<n>, written before those keys named the seat, resolves to no agent); writer is what the row is to the calling caller (see StatusResult).
  • StatusResult = { dirty: { StatusEntry }, staged: { StatusEntry }, untracked: { StatusEntry }, local_identity: string?, local_caller: string? }: local_identity is this connection's identity and local_caller the calling caller's own key. writer is "you", "other" (a caller whose session is open), "ended" (a caller whose session has ended) or "unattributed"; dirtied_by_account is the account a row's writer acts for and same_account whether that is the calling caller's; local_account is the calling connection's account. claimed_by_other_stages lists { path, stage_name, stage_row_id, owner_identity, owner_account, same_account, live, session, staged_action } for each path another staging area holds; a staging call that stages one of them takes it out of that area. writer_accounts_refusal is set when the world's backend module predates the record of writers' accounts.
  • ShowResult = { commit: any, parent: string?, entries: { any }, diff: any? }
  • DiffFile = { path: string?, action: string?, is_text: boolean?, added: number?, removed: number?, hunks: { any }? }
  • TrashRow = { row_id: number, op_kind: string?, path: string?, destroyed_by: string?, days_left: number? }
  • StashRow = { row_id: number, branch: string?, label: string?, author_hex: string?, created_at_micros: number?, dirty_count: number?, staged_count: number? }
  • StashSnapshot = { dirty: { any }, staged: { any } }
  • WorldEntry = { guid: string?, name: string?, title: string?, visibility: string?, role: string? }
  • InstallLibraryOpts = { world: string, commit: string?, as: string?, ref: string? }
  • InstallLibraryResult = { name: string, path: string, world_guid: string, version: string }
  • InstallAssetOpts = { guid: string, at: string?, ref: string? }
  • InstallAssetDep = { path: string, guid: string, version: string? }
  • InstallAssetResult = { assets_written: number, blobs_downloaded: number, root_guid: string, root_path: string?, root_version: string?, deps: { InstallAssetDep }, pending: { string } }: blobs_downloaded counts the blobs the install fetched, and pending lists written paths that did not read back before the install's wait for them stalled

Usage

-- The prelude installs the wrapper at boot:
require("@builtin::modules.world_vcs").installInto(world)

-- After install, callers use the grafted methods on `world.*`:
world.add("/source/foo.luau")
world.add_all()
local commitId = world.commit("feat: ship widget")
local zmId, verdict = world.push()

local s = world.vcsStatus()
for _, e in ipairs(s.dirty) do print(e.path) end

local commits = world.log({ limit = 20 })
local r = world.show("--stat", commits[1].commit_id)

Parts

The module is a folder of parts, each a module of its own that init.luau requires:

  • world_vcs.vcsWire: the __spacetime_vcs.* promise wire and the readers of its answers.
  • world_vcs.history: commit trees, ancestry, revisions and the diff reads.
  • world_vcs.ignore: the .zmignore / .gitignore rules.
  • world_vcs.sceneEdits: a scene's live edits and the gate publication calls run on them.
  • world_vcs.ulid: commit ids.
  • world_vcs.worldReady: waiting for a swapped world's content to land.
  • world_vcs.statusRead, world_vcs.derive, world_vcs.derivedGate, world_vcs.finalize, world_vcs.dependencies: derived outputs, sealed commits and dependency upkeep.

Notes

  • One implicit "default" stage per (world, branch, user). world.add / world.add_all / world.unstage open-or-resume that stage idempotently. world.commit auto-closes on success. world.discard drops it without committing.
  • world.add is asset-aware: staging a path inside a composite asset (.scene, .component, .bundle, etc.) expands to the full asset family so the resulting commit's tree-hash stays consistent.
  • Commit ids are caller-allocated ULIDs generated client-side (Crockford base32, 48 bits ms timestamp + 80 bits randomness via math.random()). Not a cryptographic primitive — sufficient for client-side collision avoidance.
  • All __spacetime_vcs.* calls go through the same promise-await pipeline; ERR:... payloads are unwrapped into Luau errors.
  • world.stashDrop, world.reset and world.deleteBranch run through the destructive-affirm token mechanism, minted and affirmed in the same call.
  • world.diff and world.show mirror their git CLI counterparts' argument grammar, including --stat, --name-only, --staged, --, and commit:/path.