Versioning a graph
A graph moves. A number does not: it was produced by one shape of the graph, and if the graph can be edited afterwards the number stops meaning anything. Versioning is what keeps the two apart.
Where versions are kept
Section titled “Where versions are kept”A store is where the versions of a lineage live. Which store you have is chosen once, at the edge, and nothing after that point knows which one it got.
import chatty_lab as cl
cl.Store.in_memory() # lives as long as the object does — for testscl.Store.at("./work") # the .chatty inside this directorycl.Store.on("https://chatty-lab.com", # one lineage on a platform token=TOKEN, graph="3f47b2c8-…", workspace=TEAM)Store.at makes the .chatty if it is not there yet. Store.on is happy with either the
API root or the address of the site — it adds the /api. Its graph is the lineage id
over there, which need not be the one it has here.
A store answers two questions about a lineage directly:
store.versions("3f47b2c8-…") # [Version, …] — unorderedstore.refs("3f47b2c8-…") # [Ref(name='main', head='…'), …]A workdir
Section titled “A workdir”A Workdir is one graph’s history and a branch of it — the copy somebody works in.
it = cl.Workdir(cl.Store.at("./work"), graph="3f47b2c8-…", branch="main")
it.graph # '3f47b2c8-…'it.branch # 'main'it.store # the store it is a line of work in — the same object, not a copyCommitting
Section titled “Committing”done = it.commit(graph, {agent_id: definition}, message="the reader is stricter")
done.version.n # 2done.version.digest.short # 'a1b2c3d4e5f6'done.already # FalseThe second argument is the agent definitions the graph points at, keyed by id. They are part of what gets named: editing a prompt inside an agent is editing the graph.
Commit the same content again and nothing is written — the answer comes back with
already set, holding the version that was there already.
Before you commit
Section titled “Before you commit”status() answers would this write anything, and what would it grow out of:
where = it.status(graph, agents)
where.branch # 'main'where.head # Version | None — None before the branch's first commitwhere.holding # Version | NoneThree shapes, told apart by two fields:
holding | head | Means |
|---|---|---|
is head | a version | Nothing to commit. |
None | a version | Unsaved work — this content is not in the lineage. |
None | None | The branch has not been born yet. |
| some other version | a version | The lineage already holds this content; it is simply not this branch’s head. |
Reading the history
Section titled “Reading the history”it.log() # everything, newest firstit.log("main~3..main") # a rangeit.log("stricter") # the history back from that revisionA revision is a revspec: a branch name, HEAD, ~n back from either, a range with ..,
or a digest.
it.at("HEAD") # Versionit.at("main~2") # Versionheld = it.content("HEAD~1")held.graph # the blobheld.agents # the definitions it pinned, in reduced formheld.version # which version this iscontent() is chatty checkout without a directory. The definitions come back in the
reduced form a version keeps, which is what commit wants — reducing an already-reduced
one changes nothing, so a round trip is safe.
What a version is
Section titled “What a version is”id | this store’s id for it — not the identity |
digest | what it is, and the only thing that means the same in two different stores |
kind | graph, dataset or job |
parent_ids | one ordinarily, two for a merge, none for the first |
op | edit, merge, fork, import, … |
op_params | whatever that verb recorded |
message | what somebody said it was for |
n | its place in the lineage as this store counts it — what makes it “v3” out loud |
created_by, created_at | who and when |
Branching
Section titled “Branching”strict = it.start("stricter", at="HEAD~2") # name an old version, and work therestrict.commit(narrower, message="a reader that refuses more")
it.on("stricter") # the same history, another line of itstart() is the verb a commit cannot stand in for. Committing at content the lineage
already holds is idempotent, so go back to v2 and start a variant there has no way to be
said by committing. Branch names take letters, digits and ., _, /, - — enough for
a revspec to tell a name from a range or a digest.
Comparing
Section titled “Comparing”d = it.diff("main", "stricter")
bool(d) # False when nothing movedd.unchanged # how many elements are the samed.not_comparable # the one number worth reading firstfor c in d.changes: c.id # the node or edge c.findings # ['added'], ['changed', 'downstream'], ['gone'], ['moved'], … c.fields # which parts differ — only beside 'changed' c.because_of # the upstream nodes whose keys moved: *why*, not only *that*not_comparable is first for a reason: results either side of those elements are not
results about the same thing, so a diff that looks small can still invalidate every
comparison you were about to make.
d.about carries findings about the whole thing rather than an element of it. It is
always empty for a graph, which is its nodes.
Merging
Section titled “Merging”got = it.merge("stricter", message="bring it in")
got.landed # does this branch now hold what came out?got.already # there was nothing on the other side that is not heregot.fast_forwarded # the branch simply moved; no version was writtengot.version # None when nothing was writtengot.clashes # what somebody has to choose a side for — never partialWhen there are clashes, nothing is written. Answer them and call again:
if got.clashes: for c in got.clashes: c.id, c.part # 'reader', 'node' c.ours, c.theirs # None on one side means that line deleted what the other edited c.fields # which parts the two lines disagree about
it.merge( "stricter", message="bring it in, keeping our reader", choose={"reader": "ours", "e_input__reader": "theirs"}, )choose is keyed by id across both halves of the graph. agents= on a merge is what the
definitions are now, and wins over what either version pinned.
When it refuses
Section titled “When it refuses”Every exception below inherits ChattyError, so one name catches the lot.
| Exception | Raised when |
|---|---|
GraphError | The graph handed in is not one the engine could run. |
CommitError | The version could not be decided. |
BranchError | That name could not be given to a version. |
RevisionError | That revspec does not point at anything here. |
MergeError | These two lines cannot be brought together at all. |
StoreError | The store could not answer. |
SyncError | This line of work could not be carried to the other store. |
PlatformError | The platform’s HTTP API refused — carries .status. |