Documentation
Review and restore a project change
Record the working database, isolate a proposal branch, review the diff, run committed checks, and merge or restore with clear side effects.
Separate saved resources from committed history
Saving a project, drawing, or toolpath updates the SQLite working copy. Creating a project commit snapshots the working resources into Git-compatible objects and moves the current branch ref. Those are separate operations: a saved drawing can be uncommitted, and a committed program can remain local without any remote mirror.
The sidecar's history service builds one canonical JSON file per resource. The tree includes project.json, drawings, components, toolpaths, referenced materials/tools/machine configurations, and production costs. Workflow YAML and other carried non-entity text live in the repository tree, rather than ordinary drawing records. A change to one entity does not require treating the entire database as an opaque binary diff.
Choose a disposable or intended project before following the mutating examples. Set PROJECT_ID to its real identifier and use a current connection URL/application bearer. Project history and workflow endpoints are documented in the history API reference; project metadata writes use the projects API.
Objects use git's byte-exact loose-object format, so commit and tree IDs match what git computes for the same content. Branch references live under refs/heads/, and the default branch is main.
Inspect identity and working state
Read access, commits, branch creation, opening pull requests, reviews, and workflow dispatch require access to the project. Checkout, restore, remote configuration, push/pull, and merging require maintainer access: the project owner or organization ADMIN/MANAGER. An optional account session header selects the acting user; without a valid account session the application uses its local profile.
curl --fail-with-body \
-H "Authorization: Bearer $CADCAMFUN_TOKEN" \
"$CADCAMFUN_URL/api/projects/$PROJECT_ID/vcs/status" The response names the branch, nullable HEAD, and dirty paths. Dirty means that current resource files differ from HEAD. A non-empty list is useful review information, not a storage failure. Before moving the working copy, decide whether those changes belong in a commit or should be deliberately restored/discarded.
If HEAD is null, record the first project commit before creating a branch from HEAD. The branch API resolves a real source commit; there is no placeholder commit to branch from in a new project.
Create a baseline or record intended edits
A commit requires a non-empty message and captures the current database-backed resources. Review dirty paths first: the API does not stage individual entity files. The optional files field adds safe workflow definitions, described later; it does not select which drawing edits to include.
curl --fail-with-body \
-H "Authorization: Bearer $CADCAMFUN_TOKEN" \
-H "Content-Type: application/json" \
--data '{"message":"Record baseline blank"}' \
"$CADCAMFUN_URL/api/projects/$PROJECT_ID/vcs/commit" The response is a 201 commit record with object ID, parent IDs, message, and author information. Record its OID for a later comparison or restore. An unchanged tree is rejected with NothingToCommit rather than manufacturing another revision from the same content.
authorKind defaults to user. Assistant-originated commits use ai and cannot commit on main. Actor policy is enforced by the project service; a different display message does not bypass it. Creating a commit can trigger workflows after the commit is recorded. A subsequent failed workflow does not undo that commit.
Create and check out a proposal
Creating a branch writes a ref; it does not switch the working database. Checkout is explicit and refuses uncommitted working changes. The following calls create a branch from the current HEAD and load it as the checked-out branch.
curl --fail-with-body \
-H "Authorization: Bearer $CADCAMFUN_TOKEN" -H "Content-Type: application/json" \
--data '{"name":"proposal/blank-label"}' \
"$CADCAMFUN_URL/api/projects/$PROJECT_ID/vcs/branches"
curl --fail-with-body \
-H "Authorization: Bearer $CADCAMFUN_TOKEN" -H "Content-Type: application/json" \
--data '{"branch":"proposal/blank-label"}' \
"$CADCAMFUN_URL/api/projects/$PROJECT_ID/vcs/checkout" To start elsewhere, add a from revision to branch creation. Names must be safe refs: traversal, repeated slashes, a trailing slash/.lock suffix, and ref-expression fragments are rejected. Reusing an existing name is also refused.
After checkout, save the intended project changes through their resource APIs. For a small demonstration, update project metadata with PUT /api/projects/:id and a new name, then commit it. The resulting diff changes project.json; it does not imply a geometry change. In a real part revision, save the drawing and any regenerated toolpaths before committing.
Read the exact changes under review
GET /vcs/log supports rev, limit, and an optional tracked path. GET /vcs/diff requires to and accepts an optional from. Use explicit OIDs for a reproducible comparison. Encode branch names and JSON paths as query values rather than splicing unescaped punctuation into URLs.
curl --fail-with-body --get \
-H "Authorization: Bearer $CADCAMFUN_TOKEN" \
--data-urlencode "from=main" \
--data-urlencode "to=proposal/blank-label" \
"$CADCAMFUN_URL/api/projects/$PROJECT_ID/vcs/diff" Entity changes include path, resource kind, added/modified/deleted status, optional ID/name, and property changes described by JSON pointers. Canonical JSON sorts object keys recursively while preserving array order. Non-overlapping object-property edits can merge structurally; incompatible concurrent array edits are atomic conflicts, not element-by-element reconciliations.
Review manufacturing evidence as part of the toolpath record: matching G-code and artifact identity matters more than a familiar filename. A project commit captures saved data; it does not run a missing native manufacturing request just because the tree contains NC text.
Commit executable checks with the project
The public commit endpoint's extra files are restricted to .cadcamfun/workflows/<safe-name>.yml or yaml. It accepts text values, not arbitrary filesystem patches. You cannot use this field to replace drawing JSON, write shell scripts, or delete a file by sending null.
A definition is committed as YAML text, then the runner loads it from that committed ref. The example below validates structural files, requires NC for existing toolpaths, checks operation sequencing, and attempts replay for toolpaths carrying a stored generator request.
name: Review checks
on:
manual: true
pullRequest:
branches: [main]
jobs:
inspect:
steps:
- id: structure
uses: project/validate
- uses: project/require-gcode
- uses: cam/sequence
- id: replay
uses: cam/verify To commit it from a script, construct the JSON body with a serializer rather than hand-escaping YAML:
const body = {
message: "Add committed review checks",
files: { ".cadcamfun/workflows/review.yml": await Bun.file("review.yml").text() }
}
const encoded = JSON.stringify(body) The commit still snapshots all current working resources. Validate the YAML before recording it: the history commit can succeed while the resulting invalid workflow is later recorded as a failed run. The production engine checks schema, dependencies, cycles, duplicate step IDs, and registered uses identifiers.
Dispatch checks and interpret their evidence
POST /api/projects/:id/actions/dispatch accepts an optional ref and runs every manual workflow matching that committed ref. It is not a single-workflow-name selector. Read definitions at actions/workflows and persisted outcomes at actions/runs. The service returns the latest 50 runs by default.
A run records workflow, trigger, ref, SHA, timestamps, status, and events. Jobs follow dependency levels; independent jobs can run concurrently, while steps within a job are sequential. Unknown uses identifiers fail before execution. An if value is success, failure, or always, not an arbitrary expression. A failed dependency skips its downstream job entirely.
Interpret CAM outputs carefully. cam/verify and cam/gcode inspect committed Toolpath.data.request. Missing requests, unsupported toolpaths, or missing NC may be reported in skipped outputs. A successful run with verified=0 is not verification evidence. project/require-gcode checks existing toolpaths; a project with no toolpaths can satisfy it vacuously.
Automatic HTTP push-trigger integration currently supplies no changed paths, so a push paths filter does not match those events. Use branch-only filters or explicit manual dispatch for this integration. HTTP live started/finished events are published after the synchronous run returns; use saved runs for durable results.
The writing workflow step reads current state
The current vcs/commit step delegates to project commit without a clean-working-copy guard. It commits current working database resources on the checked-out branch, which can differ from the run's ref. Its allowed extra files do not prevent that broader snapshot side effect. Use it only when that current branch and working state are intended to be committed.
Open, review, and merge a proposal
Open a local pull request with title, head, optional body, and base (main by default). Head and base must differ, and a base already containing the head has nothing to merge. The response includes a project-local number used by review and merge endpoints.
curl --fail-with-body \
-H "Authorization: Bearer $CADCAMFUN_TOKEN" -H "Content-Type: application/json" \
--data '{"title":"Review blank label","head":"proposal/blank-label","base":"main","body":"Metadata change and committed checks."}' \
"$CADCAMFUN_URL/api/projects/$PROJECT_ID/pulls" GET /pulls/:number returns the request, reviews, and changes. Reviews accept comment, approve, or request-changes, with optional path/pointer. The latest non-comment verdict per reviewer controls the request-changes block. A later comment does not clear it; that reviewer must submit an updated approve verdict. The implementation has no positive approval-count requirement when no blocking verdict remains.
POST /pulls/:number/merge requires maintainer access. AI actors cannot merge into main. If the base is checked out, the service requires a clean working copy and applies the merged snapshot to the database. If another branch is checked out, it updates the base ref without replacing that current working database. Merges may fast-forward or create a merge commit; inspect ancestry rather than assuming two parents.
Merged pull-request diffs use the recorded merge commit, preserving the review view after refs move. Local pull requests are application review records, not automatically GitHub pull requests.
Restore content without rewriting history
POST /vcs/restore takes a rev and optional paths. It requires maintainer access and a clean working copy, and creates a new commit containing the earlier content. It does not reset the branch backward and erase intervening commits. The service then applies the new HEAD to the working database.
{
"rev": "earlier-commit-oid",
"paths": ["drawings/actual-drawing-id.json"]
} Replace the path with a real tracked resource and inspect its diff first. Omitting paths restores the full earlier tree; that can replace or remove multiple related resources. Whole-project snapshots also carry referenced library items and costs so restored toolpaths can retain dependencies. Resource-version restore and project-history restore have different scopes.
After restoring manufacturing source, inspect whether saved native output still matches the intended drawing revision. A history operation preserves the earlier artifact; it does not transform that artifact into evidence for a newly modified drawing.
Mirror committed history and recover conflicts
A GitHub mirror is optional. Set remote to an owner/repo string with PUT /vcs/remote, or null to detach. Push and pull use the user's GitHub token in x-github-token for that call; it is not saved in project JSON. Maintainer access is required for these HTTP operations.
Push uploads reachable objects and refuses to overwrite unpulled remote commits. Pull requires a clean working copy, fetches the remote branch, and merges. A structural JSON conflict identifies paths and pointers without writing a partial repository merge commit. Inspect both sides, create a deliberate resolution, and retry instead of forcing one side over the other.
HTTP 409 usually means dirty state or merge conflict; 403 can mean a maintainer or AI-main restriction; 404 can mean inaccessible project or unknown revision; 400 covers invalid names, empty messages, or no changes. A database failure maps to a server storage error. Preserve the OIDs and failure message when diagnosing a mismatch.
Source provenance: packages/vcs/src/repository.ts, json.ts, snapshot.ts, github.ts; apps/sidecar/src/services/project-vcs.ts, repos/project-vcs-repo.ts, and http/vcs.ts; workflow schema/runner in packages/workflow/src and sidecar workflow-runner.ts/workflow-steps.ts.
How reviews decide a merge
- Open a pull request from a feature branch (an AI actor must use an
ai/*branch) intomain. - Reviewers submit
comment,approve, orrequest-changes, optionally anchored to a filepathand a JSONpointer. - At merge time, the service takes each reviewer's latest non-comment verdict. If any of them is
request-changes, the merge fails withblocked: "A reviewer requested changes". - If the base branch is checked out, its working copy must be clean.
- The merge is three-way per JSON property. Conflicts are returned with their paths and pointers, and nothing is committed.
- Workflows with
on.pullRequestmatching the base branch run when the pull request is opened, so their results are available during review.
| Situation | What to do |
|---|---|
blocked for an AI actor on main | Commit on an ai/* branch and open a pull request; a person merges it. |
conflict on /width of drawings/d1.json | Decide the value on one branch, commit, and merge again. |
dirty | Commit or discard local edits first; automated steps never absorb unfinished work. |
nothing-to-commit | The saved resources already match the last commit. |
Restore recipes
// Bring back one drawing as it was three commits ago
const old = repo.log()[3].oid
repo.restorePaths(old, ["drawings/d1.json"], me, "Restore plate outline")
// Make the whole tree equal to an older commit (a new commit on top)
repo.revertTo(old, me) // message defaults to "Restore <rev>"
// Who changed a toolpath, most recent first
repo.fileLog("toolpaths/op1.json").map((e) => e.message) Both restore operations add a new commit, so the older state can itself be undone and the history stays shareable. Restoring a toolpath file without the material or tool it references can leave dangling references; restore those library files in the same call.
A complete review session, captured
Every response below was captured from a running sidecar on a fresh project (ids shortened). Paths are relative to /api/projects/{id}/.
# fresh project with one drawing
GET vcs/status
{"branch":"main","head":null,"dirty":["drawings/2f315a02-….json","project.json"]}
POST vcs/commit {"message":"Baseline"}
{"oid":"8bdded63…","parents":[],"message":"Baseline\n","author":{"name":"Local user",…}}
POST vcs/branches {"name":"thicker"}
{"current":"main","branches":{"main":"8bdded63…","thicker":"8bdded63…"}}
POST vcs/checkout {"branch":"thicker"}
{"branch":"thicker","head":"8bdded63…","dirty":[]}
PUT /api/drawings/{id} {"name":"Plate 12mm"}
GET vcs/status
{"branch":"thicker","head":"8bdded63…","dirty":["drawings/2f315a02-….json"]}
POST vcs/commit {"message":"Rename plate"}
{"oid":"a6869011…","parents":["8bdded63…"],…}
GET vcs/diff?from=main&to=thicker
[{"path":"drawings/2f315a02-….json","kind":"drawings","id":"2f315a02-…","name":"Plate 12mm",
"status":"modified","changes":[
{"pointer":"/name","before":"Plate","after":"Plate 12mm"},
{"pointer":"/updatedAt","before":"…09:20.069Z","after":"…09:20.168Z"}]}]
POST pulls {"title":"Thicker plate","head":"thicker","base":"main"}
{"number":1,"state":"open","authorKind":"user","mergeCommit":null,…}
POST pulls/1/reviews {"verdict":"approve","body":"Looks good"}
{"verdict":"approve","body":"Looks good","path":null,"pointer":null,…}
POST pulls/1/merge
{"number":1,"state":"merged","mergeCommit":"a6869011…",…}
POST pulls/1/merge (again)
{"_tag":"BadRequest","message":"Pull request is not open"} | What you see | What it means |
|---|---|
head: null before the first commit | The repository exists but has no commits yet; every resource is dirty |
Dirty paths such as drawings/<id>.json, project.json | The working copy is serialised as one JSON file per resource |
| A new branch points at the current head | Without from, the branch starts at the current head |
| Diff changes are JSON pointers | Changes are reported per property (/name), not per line |
updatedAt also changed | Timestamps are versioned with the resource, so every save shows up |
mergeCommit equals the branch head | main had not moved, so the merge fast-forwarded |
| Merging twice → 400 | Only open pull requests can be merged or closed |
Review and query fields
| Request | Fields |
|---|---|
POST pulls/{number}/reviews | verdict (required): comment, approve or request-changes; optional body, and path + pointer to anchor the review on one property. Any other verdict is rejected with 400. |
GET pulls/{number} | Returns pullRequest, its reviews and the structural changes between base and head |
GET pulls?state=… | Filter the list by state |
GET vcs/log?rev=&limit=&path= | History from a revision, limited, optionally for one resource path |
GET vcs/diff?from=&to= | Structural diff between two revisions or branches |
POST vcs/restore | rev (required), optional paths. An unknown revision answers 404 Unknown revision <rev>. |
GET/PUT vcs/remote, POST vcs/push, POST vcs/pull | Configure the mirror remote and push or pull a branch |