graflo.architecture.evolution.history¶
The commit DAG, and commits on disk.
RevisionChain could only be a line, and it raised when two commits shared
a parent. That is the wrong response: a fork is something that happened, and a
history that refuses to represent it cannot be the record of what happened. The
server had the same defect from the other direction -- it silently dropped a
recording that did not extend the head, so a fork was simply lost.
Here a fork is a recorded fact. :class:History validates the things that are
genuinely broken -- duplicate ids, a parent that does not exist, a first-parent
edge whose trees do not line up -- and represents everything else, including
multiple heads.
Linear histories stay easy: :meth:History.linearize returns the single path
when there is one, and checkout replays first-parent edges, which is the
same walk the old chain did.
Attributes¶
__all__ = ['FileCommitStore', 'History', 'checkout', 'verify_history']
module-attribute
¶
logger = logging.getLogger(__name__)
module-attribute
¶
Classes¶
FileCommitStore
¶
Commits on disk, one YAML file per commit.
Filenames carry a topological index so a directory listing reads roughly in order, but the index is not authoritative: the loader rebuilds the DAG from the recorded parent ids. That is the difference from the old store, which reconstructed a chain by walking a linear parent map and raised the moment two commits shared a parent.
Source code in graflo/architecture/evolution/history.py
Attributes¶
root = Path(root)
instance-attribute
¶
Methods:¶
__init__(root='.graflo/commits')
¶
append(commit)
¶
Add one commit to the stored history, validating the DAG.
Source code in graflo/architecture/evolution/history.py
load()
¶
Read every stored commit and assemble the DAG.
Source code in graflo/architecture/evolution/history.py
save(history)
¶
Write history, replacing whatever was there.
Source code in graflo/architecture/evolution/history.py
History
¶
Bases: ConfigBaseModel
A set of commits forming a directed acyclic graph.
Source code in graflo/architecture/evolution/history.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 | |
Attributes¶
commits = PydanticField(default_factory=list)
class-attribute
instance-attribute
¶
reversible
property
¶
Whether every commit in the history can be inverted.
Methods:¶
ancestors(commit_id, *, include_self=False)
¶
Every commit reachable by following parents from commit_id.
Source code in graflo/architecture/evolution/history.py
children_of(commit_id)
¶
extend(commit)
¶
first_parent_path(commit_id)
¶
Root → commit_id along first-parent edges, oldest first.
This is the walk a checkout replays. Following only first parents is what makes it work for merge commits too: their ops were materialized against exactly this parent.
Source code in graflo/architecture/evolution/history.py
get(commit_id)
¶
The commit with commit_id, or None.
Accepts an unambiguous prefix, the way git does.
Source code in graflo/architecture/evolution/history.py
heads()
¶
Commits no other commit names as a parent, oldest-first by topology.
More than one head means the history has forked -- a fact to report, not an error to raise.
Source code in graflo/architecture/evolution/history.py
linearize()
¶
The single path through a linear history, oldest first.
Raises:
| Type | Description |
|---|---|
CommitError
|
The history has forked or has several roots, so there
is no single path. Use :meth: |
Source code in graflo/architecture/evolution/history.py
ops_between(ancestor, descendant)
¶
The ops replayed from ancestor to descendant, oldest first.
Follows first parents, as a checkout does. None when ancestor is
not on that path.
Source code in graflo/architecture/evolution/history.py
require(commit_id)
¶
Like :meth:get, but raises when there is no such commit.
Source code in graflo/architecture/evolution/history.py
roots()
¶
topological()
¶
All commits, parents before children, deterministic on ties.
Ties break on commit id, so the same history always linearizes the same way -- a log that reorders itself between runs is not a log.
Source code in graflo/architecture/evolution/history.py
Functions:¶
checkout(base, history, commit_id=None, *, verify=True, finish_init=False)
¶
The manifest as of commit_id, replayed from base.
Replays first-parent edges from the root, verifying every recorded tree on the way. Verification is the point: a history that no longer describes the manifest it was generated from fails here instead of producing a plausible but wrong result.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base
|
GraphManifest
|
The manifest the root commit starts from. |
required |
history
|
History
|
The commit DAG. |
required |
commit_id
|
str | None
|
Where to stop. |
None
|
verify
|
bool
|
Check each recorded tree hash against the replayed manifest. |
True
|
finish_init
|
bool
|
Run |
False
|
Raises:
| Type | Description |
|---|---|
CommitError
|
An unknown commit, a forked history with no commit named, or a replay that did not reproduce a recorded tree. |
Source code in graflo/architecture/evolution/history.py
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 | |
checkout_parent(base, history, commit_id, *, verify=True)
¶
The manifest commit_id's operations were applied to.
That is its first parent's manifest, or base for a root commit. An operation's inverse is computed against this state, not the one after it.
Raises:
| Type | Description |
|---|---|
CommitError
|
An unknown commit, a replay that did not reproduce a recorded tree, or a root whose recorded starting tree is not base. |
Source code in graflo/architecture/evolution/history.py
rehash_trees(history, bases)
¶
Recompute every recorded tree under the current canonical form.
A CANON_VERSION bump moves every content hash, so trees recorded under
the previous canon no longer verify. This replays each commit from the base
of its first-parent root and records the trees the replay produces. Ids are
kept: a commit id hashes ops and parents, not trees, and a naming root's id
stays the name its descendants already point at.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
history
|
History
|
The commit DAG. |
required |
bases
|
Mapping[str, GraphManifest]
|
The manifest each root starts from, keyed by root commit id. A naming root's base is the manifest it names. |
required |
Returns:
| Type | Description |
|---|---|
History
|
The rewritten history, and the ids of the commits left as they were |
list[str]
|
because no base was given for their root. |
Source code in graflo/architecture/evolution/history.py
verify_history(base, history)
¶
Replay every head and report what fails, instead of raising on the first.
Returns a list of human-readable problems -- empty when the whole DAG
replays cleanly. Useful as a health check over a history that may have
several heads, where checkout would refuse to pick one.
A merge joins two lineages, so a history can hold more than one root and
one base cannot replay both. A head descending from a different root is
reported as not verifiable from this base rather than as a failure --
and reported rather than skipped, because a head nobody checked is exactly
what a health check must not pass over in silence.