Builtin Library
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'sreleased(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 inwrittenand 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 itsreason),released(each path taken from another staging area, with that area asstage, its row id asstage_row_id, itsaccount,same_account, itssessionand whether that session islive) andwritten(each staged path a caller of another account is writing now, withbyandaccount). 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 (HEADitself) 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.stagenames 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, andignored_summary: the ignored rows counted by kind, one entry per scene autosave overlay (kind = "overlay",rootitsscene_dirtyfolder) and per.zmignore/.gitignoreline that matches rows (kind = "rule",file,rule), each with itscount.{ ignored = true }lists the ignored rows themselves inignored, 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 withreason = "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 (astagedentry withreferences_only = true), so another caller's unstaged edit of the file stays out of the commit.world.commitrefuses a commit holding a file whose table pins a version the commit leaves out (a changed asset or pin marker left unstaged), with adep.stale_referencesentry per file. (NamedvcsStatus, notstatus:world.status()is the runtime-snapshot accessor fromworld_status.module.)world.log(opts: LogOpts?) -> { CommitRow }—git log: every commit reachable fromopts.rev(defaultHEAD) through both parents, newest first;opts.pathkeeps the commits that touched one file.world.revParse(rev: string) -> string—git rev-parse: the commitHEAD, a branch,origin/<branch>, a commit id or a unique prefix names, with~N/^/^2after 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/.gitignorerule matches are left out unless--ignoredis 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 branchand 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 mergeinto 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 underkept, with its reason inreasons.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;refis a row id orstash@{n}. A drop snapshots the stash to trash.world.reset(target: string, opts: ResetOpts?) -> ResetResult—git reset --soft | --mixed | --hardto any revision; the commits the head leaves behind become trash entries.hardtakes 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 inkept.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 (@builtinincluded) to the commitref(defaultmain) resolves to in the library's world: rewrites the marker, waits until the engine holds the library at that commit (an@builtinpin 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:keptlists the ones the new commit left as they were,conflictsthe 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'srequested_ref(defaultmain) resolves in the library's world to a commit other than the marker'sresolved_commit({ kind = "library", library, from, to }; apply withworld.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 withworld.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.pullAssetaimed 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) andto(what the origin holds atopts.ref, default the refworld.checkUpdatescompares it with), thencommits, the commits of the origin world in between, newest first, each{ id, message, author, date }(moreis true past 200), then the files;modifiednames up front each file holding an edit of the world's own that the update touches, withmergesorconflicts, andby, 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 inmodified, 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, andfiles: each file the update adds, changes or removes,{ path, change, diff, hunks, local_edit },local_editbeingnone,mergesorconflictsfor 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 = trueaddsfiles, the list behind the counts; the moving files are read from the engine's reverse reference index.opts.commits = falsereads no commits andopts.files = falseleaves an install'sfilesand diffs out, so{ commits = false, files = false }is the check before an update:modifiedand 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 inmodified. 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.worldopclosure,maxDepth = 0), or at the commit that adds the root's path; when a version cannot be read the list stops there,moreis true andcommits_errorsays why. The origin's log, commits and diffs are read through the trusted ZeroMind bridge (zeromind.worldopscommits,commit,diff).world.pull(branch: string?, opts: PullOpts?) -> PullResult—git pull: fetchorigin/<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'sbehindcount answers that, for another branch the session branch's history does.opts.ffOnlyrefuses anything but a fast-forward,opts.noFfalways makes a merge commit.world.update(units: (string | { string })?, opts: UpdateOpts?) -> UpdateResult— apply updates: nounitsupdates every unitworld.checkUpdateslists, a name or a list of names (library names and install root paths, mixed) updates those, toopts.refwhen 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 inworld.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 atpathwith 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 independencies.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.modulealso 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 forinstallInto.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_byis the identity the row was written under, which every session of one account shares;dirtied_by_calleris the caller key of the call that wrote it (client:<session>/actor:<sub>, andclient:<session>@<engine seat>for an agent an engine hosts in its dock, whose session id every engine repeats; a row keyed by a bareclient:ide-slot-<n>, written before those keys named the seat, resolves to no agent);writeris what the row is to the calling caller (seeStatusResult).StatusResult = { dirty: { StatusEntry }, staged: { StatusEntry }, untracked: { StatusEntry }, local_identity: string?, local_caller: string? }:local_identityis this connection's identity andlocal_callerthe calling caller's own key.writeris"you","other"(a caller whose session is open),"ended"(a caller whose session has ended) or"unattributed";dirtied_by_accountis the account a row's writer acts for andsame_accountwhether that is the calling caller's;local_accountis the calling connection's account.claimed_by_other_stageslists{ 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_refusalis 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_downloadedcounts the blobs the install fetched, andpendinglists 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/.gitignorerules.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.unstageopen-or-resume that stage idempotently.world.commitauto-closes on success.world.discarddrops it without committing. world.addis 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.resetandworld.deleteBranchrun through the destructive-affirm token mechanism, minted and affirmed in the same call.world.diffandworld.showmirror their git CLI counterparts' argument grammar, including--stat,--name-only,--staged,--, andcommit:/path.