src/History.bend

ber-core-store@0.1.1.0 · 495 lines · 0 definitions

1import Base
2import ber-core-store@0.1.1.0/src/Reading.bend as Reading
3import ber-core-store@0.1.1.0/src/LogicalKey.bend as LogicalKey
4import ber-core-store@0.1.1.0/src/ContentHash.bend as ContentHash
5import ber-core-store@0.1.1.0/src/StateTree.bend as StateTree
6import ber-core-store@0.1.1.0/src/Staging.bend as Staging
7import ber-core-store@0.1.1.0/src/ContentObject.bend as ContentObject
8import ber-core-store@0.1.1.0/src/JsonAdapter.bend as JsonAdapter
9import ber-core-store@0.1.1.0/src/Value.bend as Value
10import ber-core-store@0.1.1.0/src/Store.bend as Store
11import bend-kit-json@0.3.0.0/json.bend as Json
12
13# Linear history: staging -> merged tree -> commit -> per-key version index.
14# Commits serialize as canonical JSON without timestamp/certificate (both
15# re-materialize on load: 0 / None). Loads re-serialize the parsed fields and
16# compare against the stored text: a mismatch yields None, never corruption.
17# Kit is named for the Val TYPE only; operations go through JsonAdapter.
18# Effects are Store Op actions composed in do-blocks. Reads resolve to Values.
19
20# One metadata key/value field.
21type CommitMeta is Data:
22  Field{field_key: String, field_value: String}
23
24# Identifier, parents, tree hash, timestamp, metadata and certificate.
25type Commit is Data:
26  Make{commit_identifier: String, parent_commit_identifiers: List<&2, String>, tree_hash: String, timestamp: Nat, metadata: List<&2, CommitMeta>, certificate_hash: Maybe<&2, String>}
27
28# Projects the identifier out of a commit.
29def commit_identifier_of(commit_value: Commit) -> String:
30  match commit_value:
31    case Make{commit_identifier, parent_commit_identifiers, tree_hash, timestamp, metadata, certificate_hash}:
32      commit_identifier
33
34# Projects the tree hash out of a commit.
35def commit_tree_of(commit_value: Commit) -> String:
36  match commit_value:
37    case Make{commit_identifier, parent_commit_identifiers, tree_hash, timestamp, metadata, certificate_hash}:
38      tree_hash
39
40# Encodes one metadata field as a two-item array.
41def meta_pair_to_json(meta_field: CommitMeta) -> Json.Val:
42  match meta_field:
43    case Field{field_key, field_value}:
44      JsonAdapter.make_arr(Con{JsonAdapter.make_str(field_key), Con{JsonAdapter.make_str(field_value), Nil{}}})
45
46# Encodes the metadata list.
47def meta_list_to_json(metadata: List<&2, CommitMeta>) -> List<&2, Json.Val>:
48  match metadata:
49    case Nil{}:
50      Nil{}
51    case Con{head_field, remaining_fields}:
52      Con{meta_pair_to_json(head_field), meta_list_to_json(remaining_fields)}
53
54# Encodes parent identifiers as strings.
55def encode_parent_list(parent_commit_identifiers: List<&2, String>) -> List<&2, Json.Val>:
56  match parent_commit_identifiers:
57    case Nil{}:
58      Nil{}
59    case Con{head_parent, remaining_parents}:
60      Con{JsonAdapter.make_str(head_parent), encode_parent_list(remaining_parents)}
61
62# Encodes the parents/tree/meta document canonically.
63def finalize_commit_document(parents_field: Sigma<&2, &2, String, _ => Json.Val>, tree_field: Sigma<&2, &2, String, _ => Json.Val>, meta_field: Sigma<&2, &2, String, _ => Json.Val>) -> String:
64  JsonAdapter.encode_canonical(JsonAdapter.make_obj(Con{parents_field, Con{tree_field, Con{meta_field, Nil{}}}}))
65
66# Serializes parents, tree and metadata; the hashed bytes.
67def serialize_commit(parent_commit_identifiers: List<&2, String>, metadata: List<&2, CommitMeta>, tree_hash_value: String) -> String:
68  finalize_commit_document(JsonAdapter.make_kv("parents", JsonAdapter.make_arr(encode_parent_list(parent_commit_identifiers))), JsonAdapter.make_kv("tree", JsonAdapter.make_str(tree_hash_value)), JsonAdapter.make_kv("meta", JsonAdapter.make_arr(meta_list_to_json(metadata))))
69
70# Conses a decoded meta field onto the tail.
71def prepend_meta_field(field_key: String, field_value: String, parsed_tail: Maybe<&2, List<&2, CommitMeta>>) -> Maybe<&2, List<&2, CommitMeta>>:
72  match parsed_tail:
73    case None{}:
74      None{}
75    case Some{tail_fields}:
76      Some{Con{Field{field_key, field_value}, tail_fields}}
77
78# Unwraps the field value, then prepends.
79def prepend_meta_value(field_key: String, value_maybe: Maybe<&2, String>, parsed_tail: Maybe<&2, List<&2, CommitMeta>>) -> Maybe<&2, List<&2, CommitMeta>>:
80  match value_maybe:
81    case None{}:
82      None{}
83    case Some{field_value}:
84      prepend_meta_field(field_key, field_value, parsed_tail)
85
86# Unwraps the key text, then continues.
87def prepend_meta_texts(key_maybe: Maybe<&2, String>, value_maybe: Maybe<&2, String>, parsed_tail: Maybe<&2, List<&2, CommitMeta>>) -> Maybe<&2, List<&2, CommitMeta>>:
88  match key_maybe:
89    case None{}:
90      None{}
91    case Some{field_key}:
92      prepend_meta_value(field_key, value_maybe, parsed_tail)
93
94# Decodes one [key, value] array; None on wrong shape.
95def prepend_meta_items(pair_items: List<&2, Json.Val>, parsed_tail: Maybe<&2, List<&2, CommitMeta>>) -> Maybe<&2, List<&2, CommitMeta>>:
96  match pair_items:
97    case Con{key_json, Con{value_json, Nil{}}}:
98      prepend_meta_texts(JsonAdapter.as_str(key_json), JsonAdapter.as_str(value_json), parsed_tail)
99    case _:
100      None{}
101
102# Unwraps the pair array, then decodes.
103def prepend_parsed_meta(pair_maybe: Maybe<&2, List<&2, Json.Val>>, parsed_tail: Maybe<&2, List<&2, CommitMeta>>) -> Maybe<&2, List<&2, CommitMeta>>:
104  match pair_maybe:
105    case None{}:
106      None{}
107    case Some{pair_items}:
108      prepend_meta_items(pair_items, parsed_tail)
109
110# Decodes one metadata item.
111def prepend_meta_entry(head_item: Json.Val, parsed_tail: Maybe<&2, List<&2, CommitMeta>>) -> Maybe<&2, List<&2, CommitMeta>>:
112  prepend_parsed_meta(JsonAdapter.as_arr(head_item), parsed_tail)
113
114# Decodes the metadata array; None on any bad item.
115def parse_meta_list(meta_items: List<&2, Json.Val>) -> Maybe<&2, List<&2, CommitMeta>>:
116  match meta_items:
117    case Nil{}:
118      Some{Nil{}}
119    case Con{head_item, remaining_items}:
120      prepend_meta_entry(head_item, parse_meta_list(remaining_items))
121
122# Accepts only when re-serialization matches stored bytes.
123def verify_commit_text(text_matches: Bool, +requested_identifier: String, +parent_list: List<&2, String>, +tree_text: String, +parsed_meta: List<&2, CommitMeta>) -> Maybe<&2, Commit>:
124  match text_matches:
125    case True{}:
126      Some{Make{requested_identifier, parent_list, tree_text, 0n, parsed_meta, None{}}}
127    case False{}:
128      None{}
129
130# Continues commit extraction once metadata decodes.
131def finish_commit_meta(+requested_identifier: String, +stored_text: String, +parent_list: List<&2, String>, +tree_text: String, meta_maybe: Maybe<&2, List<&2, CommitMeta>>) -> Maybe<&2, Commit>:
132  match meta_maybe:
133    case None{}:
134      None{}
135    case Some{+parsed_meta}:
136      verify_commit_text(String.eq(serialize_commit(parent_list, parsed_meta, tree_text), stored_text), requested_identifier, parent_list, tree_text, parsed_meta)
137
138# Continues commit extraction once the meta items decode.
139def finish_commit(+requested_identifier: String, +stored_text: String, +parent_list: List<&2, String>, +tree_text: String, meta_items_maybe: Maybe<&2, List<&2, Json.Val>>) -> Maybe<&2, Commit>:
140  match meta_items_maybe:
141    case None{}:
142      None{}
143    case Some{meta_items}:
144      finish_commit_meta(requested_identifier, stored_text, parent_list, tree_text, parse_meta_list(meta_items))
145
146# Unwraps the meta array, then continues.
147def finish_meta_array(+requested_identifier: String, +stored_text: String, +parent_list: List<&2, String>, +tree_text: String, meta_maybe: Maybe<&2, Json.Val>) -> Maybe<&2, Commit>:
148  match meta_maybe:
149    case None{}:
150      None{}
151    case Some{meta_json}:
152      finish_commit(requested_identifier, stored_text, parent_list, tree_text, JsonAdapter.as_arr(meta_json))
153
154# Unwraps the tree field as text.
155def tree_maybe_as_text(tree_maybe: Maybe<&2, Json.Val>) -> Maybe<&2, String>:
156  match tree_maybe:
157    case None{}:
158      None{}
159    case Some{tree_json}:
160      JsonAdapter.as_str(tree_json)
161
162# Continues commit extraction once the tree text decodes.
163def extract_commit_meta(+requested_identifier: String, +stored_text: String, +parent_list: List<&2, String>, tree_text_maybe: Maybe<&2, String>, +document: Json.Val) -> Maybe<&2, Commit>:
164  match tree_text_maybe:
165    case None{}:
166      None{}
167    case Some{tree_text}:
168      finish_meta_array(requested_identifier, stored_text, parent_list, tree_text, JsonAdapter.get_field(document, "meta"))
169
170# Continues commit extraction once parents decode.
171def extract_commit_tree(+requested_identifier: String, +stored_text: String, parent_strings_maybe: Maybe<&2, List<&2, String>>, tree_maybe: Maybe<&2, Json.Val>, +document: Json.Val) -> Maybe<&2, Commit>:
172  match parent_strings_maybe:
173    case None{}:
174      None{}
175    case Some{parent_list}:
176      extract_commit_meta(requested_identifier, stored_text, parent_list, tree_maybe_as_text(tree_maybe), document)
177
178# Unwraps the parent items, then continues.
179def extract_parents_items(+requested_identifier: String, +stored_text: String, items_maybe: Maybe<&2, List<&2, Json.Val>>, +document: Json.Val) -> Maybe<&2, Commit>:
180  match items_maybe:
181    case None{}:
182      None{}
183    case Some{parent_items}:
184      extract_commit_tree(requested_identifier, stored_text, ContentObject.extract_each_string(parent_items), JsonAdapter.get_field(document, "tree"), document)
185
186# Unwraps the parents field, then continues.
187def extract_parents_array(+requested_identifier: String, +stored_text: String, parents_maybe: Maybe<&2, Json.Val>, +document: Json.Val) -> Maybe<&2, Commit>:
188  match parents_maybe:
189    case None{}:
190      None{}
191    case Some{parents_json}:
192      extract_parents_items(requested_identifier, stored_text, JsonAdapter.as_arr(parents_json), document)
193
194# Reads the parents field of the document.
195def extract_commit_parents(+requested_identifier: String, +stored_text: String, +document: Json.Val) -> Maybe<&2, Commit>:
196  extract_parents_array(requested_identifier, stored_text, JsonAdapter.get_field(document, "parents"), document)
197
198# Builds the commit from parsed JSON; None on malformed input.
199def commit_from_parse(requested_identifier: String, stored_text: String, parse_result: Maybe<&2, Json.Val>) -> Maybe<&2, Commit>:
200  match parse_result:
201    case None{}:
202      None{}
203    case Some{+document}:
204      extract_commit_parents(requested_identifier, stored_text, document)
205
206# Parses stored commit text.
207def parse_commit_text(requested_identifier: String, +stored_text: String) -> Maybe<&2, Commit>:
208  commit_from_parse(requested_identifier, stored_text, JsonAdapter.parse_text(stored_text))
209
210# Parses loaded text; missing storage reads as None.
211def parse_commit(requested_identifier: String, stored_maybe: Maybe<&2, String>) -> Maybe<&2, Commit>:
212  match stored_maybe:
213    case None{}:
214      None{}
215    case Some{stored_text}:
216      parse_commit_text(requested_identifier, stored_text)
217
218# Loads and verifies a commit by identifier.
219def load_commit(+requested_identifier: String) -> Store.Op<&2, Maybe<&2, Commit>>:
220  do Store.Op<&2, Maybe<&2, Commit>>:
221    stored_maybe : Maybe<&2, String> <- Store.oget(LogicalKey.build_commit_storage_key(requested_identifier))
222    return parse_commit(requested_identifier, stored_maybe)
223
224# Empty Sess result of unit type.
225def empty_unit() -> Store.Op<&2, Unit>:
226  do Store.Op<&2, Unit>:
227    return Unit{}
228
229# Empty Sess result of entry-list type.
230def empty_pair_list() -> Store.Op<&2, List<&2, Reading.ChainEntry>>:
231  do Store.Op<&2, List<&2, Reading.ChainEntry>>:
232    return Nil{}
233
234# Empty Sess result of tree type.
235def empty_tree_maybe() -> Store.Op<&2, Maybe<&2, StateTree.StateTreeNode>>:
236  do Store.Op<&2, Maybe<&2, StateTree.StateTreeNode>>:
237    return None{}
238
239# Serializes, hashes and stores a commit.
240def persist_commit(+parent_commit_identifiers: List<&2, String>, +metadata: List<&2, CommitMeta>, +tree_hash_value: String) -> Store.Op<&2, Commit>:
241  do Store.Op<&2, Commit>:
242    +commit_text : String = serialize_commit(parent_commit_identifiers, metadata, tree_hash_value)
243    +commit_identifier : String = ContentHash.compute_sha256_hex(commit_text)
244    stored_unit : Unit <- Store.oput(LogicalKey.build_commit_storage_key(commit_identifier), commit_text)
245    return Make{commit_identifier, parent_commit_identifiers, tree_hash_value, 0n, metadata, None{}}
246
247# Hashes and stores a merged entry list as a tree.
248def persist_tree(+merged_entries: List<&2, Reading.ChainEntry>) -> Store.Op<&2, String>:
249  do Store.Op<&2, String>:
250    +tree_hash_value : String = StateTree.compute_tree_hash(merged_entries)
251    stored_unit : Unit <- Store.oput(LogicalKey.build_tree_storage_key(tree_hash_value), StateTree.serialize_tree(merged_entries, tree_hash_value))
252    return tree_hash_value
253
254# Writes one version-index entry per merged entry.
255def write_each_index(merged_entries: List<&2, Reading.ChainEntry>, +commit_value: Commit) -> Store.Op<&2, Unit>:
256  match merged_entries:
257    case Nil{}:
258      empty_unit()
259    case Con{+head_entry, remaining_entries}:
260      do Store.Op<&2, Unit>:
261        head_unit : Unit <- Store.oput(LogicalKey.build_version_index_key(Reading.entry_key_of(head_entry), commit_identifier_of(commit_value)), Reading.entry_value_of(head_entry))
262        tail_unit : Unit <- write_each_index(remaining_entries, commit_value)
263        return tail_unit
264
265# Removes staged keys consumed by the commit.
266def clear_stage_keys(+session_identifier: String, merged_entries: List<&2, Reading.ChainEntry>) -> Store.Op<&2, Unit>:
267  match merged_entries:
268    case Nil{}:
269      empty_unit()
270    case Con{head_entry, remaining_entries}:
271      do Store.Op<&2, Unit>:
272        cleared_unit : Unit <- Store.odel(LogicalKey.build_stage_storage_key_combined(session_identifier, Reading.entry_key_of(head_entry)))
273        cleared_tail : Unit <- clear_stage_keys(session_identifier, remaining_entries)
274        return cleared_tail
275
276# Writes the index and clears the stage in sequence.
277def persist_index_and_clear(+session_identifier: String, +merged_entries: List<&2, Reading.ChainEntry>, +commit_value: Commit) -> Store.Op<&2, Unit>:
278  do Store.Op<&2, Unit>:
279    indexed_unit : Unit <- write_each_index(merged_entries, commit_value)
280    cleared_unit : Unit <- clear_stage_keys(session_identifier, merged_entries)
281    index_cleared_unit : Unit <- Store.odel(Staging.stage_index_key(session_identifier))
282    return index_cleared_unit
283
284# Persists tree, commit and index, then clears the stage.
285def materialize_commit(+session_identifier: String, +parent_commit_identifiers: List<&2, String>, metadata: List<&2, CommitMeta>, +merged_entries: List<&2, Reading.ChainEntry>) -> Store.Op<&2, Commit>:
286  do Store.Op<&2, Commit>:
287    +tree_hash_value : String <- persist_tree(merged_entries)
288    +commit_value : Commit <- persist_commit(parent_commit_identifiers, metadata, tree_hash_value)
289    cleared_unit : Unit <- persist_index_and_clear(session_identifier, merged_entries, commit_value)
290    return commit_value
291
292# Parses loaded tree text.
293def parse_tree_text(tree_hash_value: String, tree_maybe: Maybe<&2, String>) -> Maybe<&2, StateTree.StateTreeNode>:
294  match tree_maybe:
295    case None{}:
296      None{}
297    case Some{tree_text}:
298      StateTree.parse_tree_document(tree_text)
299
300# Loads a tree by hash.
301def load_tree_text(+tree_hash_value: String) -> Store.Op<&2, Maybe<&2, StateTree.StateTreeNode>>:
302  do Store.Op<&2, Maybe<&2, StateTree.StateTreeNode>>:
303    tree_maybe : Maybe<&2, String> <- Store.oget(LogicalKey.build_tree_storage_key(tree_hash_value))
304    return parse_tree_text(tree_hash_value, tree_maybe)
305
306# Loads the tree a commit points at.
307def load_tree_by_commit(commit_maybe: Maybe<&2, Commit>) -> Store.Op<&2, Maybe<&2, StateTree.StateTreeNode>>:
308  match commit_maybe:
309    case None{}:
310      empty_tree_maybe()
311    case Some{found_commit}:
312      load_tree_text(commit_tree_of(found_commit))
313
314# Reads the tree for a commit identifier.
315def read_tree_at(+commit_identifier: String) -> Store.Op<&2, Maybe<&2, StateTree.StateTreeNode>>:
316  do Store.Op<&2, Maybe<&2, StateTree.StateTreeNode>>:
317    commit_maybe : Maybe<&2, Commit> <- load_commit(commit_identifier)
318    loaded_tree : Maybe<&2, StateTree.StateTreeNode> <- load_tree_by_commit(commit_maybe)
319    return loaded_tree
320
321# Missing base trees read as empty.
322def base_or_empty(tree_maybe: Maybe<&2, StateTree.StateTreeNode>) -> List<&2, Reading.ChainEntry>:
323  match tree_maybe:
324    case None{}:
325      Nil{}
326    case Some{parent_tree}:
327      StateTree.tree_entries(parent_tree)
328
329# Loads the first parent entries.
330def load_parent_tree(head_parent: String) -> Store.Op<&2, List<&2, Reading.ChainEntry>>:
331  do Store.Op<&2, List<&2, Reading.ChainEntry>>:
332    tree_maybe : Maybe<&2, StateTree.StateTreeNode> <- read_tree_at(head_parent)
333    return base_or_empty(tree_maybe)
334
335# Loads base entries; root commits start empty.
336def load_base_entries(parent_commit_identifiers: List<&2, String>) -> Store.Op<&2, List<&2, Reading.ChainEntry>>:
337  match parent_commit_identifiers:
338    case Nil{}:
339      empty_pair_list()
340    case Con{head_parent, remaining_parents}:
341      load_parent_tree(head_parent)
342
343# Skips missing staged values, keeps present ones.
344def prepend_pair_maybe(+head_key: String, head_value_maybe: Maybe<&2, String>, tail_pairs: List<&2, Reading.ChainEntry>) -> List<&2, Reading.ChainEntry>:
345  match head_value_maybe:
346    case None{}:
347      tail_pairs
348    case Some{head_value}:
349      Con{Reading.make_entry(head_key, head_value), tail_pairs}
350
351# Loads one staged value and prepends it.
352def prepend_loaded_pair(+session_identifier: String, +head_key: String, tail_action: Store.Op<&2, List<&2, Reading.ChainEntry>>) -> Store.Op<&2, List<&2, Reading.ChainEntry>>:
353  do Store.Op<&2, List<&2, Reading.ChainEntry>>:
354    head_value_maybe : Maybe<&2, String> <- Store.oget(LogicalKey.build_stage_storage_key_combined(session_identifier, head_key))
355    tail_pairs : List<&2, Reading.ChainEntry> <- tail_action
356    return prepend_pair_maybe(head_key, head_value_maybe, tail_pairs)
357
358# Loads every staged key in the manifest.
359def load_each_pair(+session_identifier: String, staged_keys: List<&2, String>) -> Store.Op<&2, List<&2, Reading.ChainEntry>>:
360  match staged_keys:
361    case Nil{}:
362      empty_pair_list()
363    case Con{head_key, remaining_keys}:
364      prepend_loaded_pair(session_identifier, head_key, load_each_pair(session_identifier, remaining_keys))
365
366# Collects the session staged pairs.
367def collect_staged_pairs(+session_identifier: String) -> Store.Op<&2, List<&2, Reading.ChainEntry>>:
368  do Store.Op<&2, List<&2, Reading.ChainEntry>>:
369    staged_keys : List<&2, String> <- Staging.load_stage_index(session_identifier)
370    collected_pairs : List<&2, Reading.ChainEntry> <- load_each_pair(session_identifier, staged_keys)
371    return collected_pairs
372
373# Materializes stage plus base into a new commit.
374def create_commit(+session_identifier: String, +parent_commit_identifiers: List<&2, String>, metadata: List<&2, CommitMeta>) -> Store.Op<&2, Commit>:
375  do Store.Op<&2, Commit>:
376    staged_pairs : List<&2, Reading.ChainEntry> <- collect_staged_pairs(session_identifier)
377    base_entries : List<&2, Reading.ChainEntry> <- load_base_entries(parent_commit_identifiers)
378    finished_commit : Commit <- materialize_commit(session_identifier, parent_commit_identifiers, metadata, StateTree.merge_sorted_entries(staged_pairs, base_entries))
379    return finished_commit
380
381# Versioned reads walk the first-parent chain with Nat fuel; unknown
382# commits contribute "" links that never match a real index key.
383def commit_parents_of(commit_value: Commit) -> List<&2, String>:
384  match commit_value:
385    case Make{commit_identifier, parent_commit_identifiers, tree_hash, timestamp, metadata, certificate_hash}:
386      parent_commit_identifiers
387
388# Empty Sess result of string-list type.
389def empty_string_list() -> Store.Op<&2, List<&2, String>>:
390  do Store.Op<&2, List<&2, String>>:
391    return Nil{}
392
393# Empty Sess result of entry type.
394def empty_entry_maybe() -> Store.Op<&2, Maybe<&2, Reading.ChainEntry>>:
395  do Store.Op<&2, Maybe<&2, Reading.ChainEntry>>:
396    return None{}
397
398# Empty Op result of value type.
399def empty_value_maybe() -> Store.Op<&2, Maybe<&2, Value.Value>>:
400  do Store.Op<&2, Maybe<&2, Value.Value>>:
401    return None{}
402
403# Empty Op result of text type (commit identifiers, not values).
404def empty_text_maybe() -> Store.Op<&2, Maybe<&2, String>>:
405  do Store.Op<&2, Maybe<&2, String>>:
406    return None{}
407
408# Heads the parent list; root commits yield empty.
409def first_parent_name(parent_identifiers: List<&2, String>) -> String:
410  match parent_identifiers:
411    case Nil{}:
412      ""
413    case Con{head_parent, remaining_parents}:
414      head_parent
415
416# Heads the parent list; unknown commits yield empty.
417def first_parent_text(commit_maybe: Maybe<&2, Commit>) -> String:
418  match commit_maybe:
419    case None{}:
420      ""
421    case Some{found_commit}:
422      first_parent_name(commit_parents_of(found_commit))
423
424# Walks first-parents with fuel; newest first.
425def walk_commit_chain(remaining_fuel: Nat, +commit_identifier: String) -> Store.Op<&2, List<&2, String>>:
426  match remaining_fuel:
427    case 0n:
428      empty_string_list()
429    case 1n+fuel_left:
430      do Store.Op<&2, List<&2, String>>:
431        commit_maybe : Maybe<&2, Commit> <- load_commit(commit_identifier)
432        tail_chain : List<&2, String> <- walk_commit_chain(fuel_left, first_parent_text(commit_maybe))
433        return Con{commit_identifier, tail_chain}
434
435# A stored value wins; otherwise the tail result.
436def decide_search_hit(stored_maybe: Maybe<&2, String>, +lookup_key: String, tail_result: Maybe<&2, Reading.ChainEntry>) -> Maybe<&2, Reading.ChainEntry>:
437  match stored_maybe:
438    case None{}:
439      tail_result
440    case Some{stored_value}:
441      Some{Reading.make_entry(lookup_key, stored_value)}
442
443# Searches the ancestor chain newest-first.
444def search_chain_entries(ancestor_chain: List<&2, String>, +namespace_name: String, +record_identifier: String) -> Store.Op<&2, Maybe<&2, Reading.ChainEntry>>:
445  match ancestor_chain:
446    case Nil{}:
447      empty_entry_maybe()
448    case Con{head_commit, remaining_commits}:
449      do Store.Op<&2, Maybe<&2, Reading.ChainEntry>>:
450        +lookup_key : String = Reading.encode_lookup_key(namespace_name, record_identifier)
451        stored_maybe : Maybe<&2, String> <- Store.oget(LogicalKey.build_version_index_key(lookup_key, head_commit))
452        tail_result : Maybe<&2, Reading.ChainEntry> <- search_chain_entries(remaining_commits, namespace_name, record_identifier)
453        return decide_search_hit(stored_maybe, lookup_key, tail_result)
454
455# Projects the payload out of a loaded object.
456def payload_or_empty(loaded_maybe: Maybe<&2, ContentObject.ContentObject>) -> Maybe<&2, Value.Value>:
457  match loaded_maybe:
458    case None{}:
459      None{}
460    case Some{loaded_object}:
461      Some{ContentObject.object_payload_of(loaded_object)}
462
463# Loads an object payload by content hash.
464def load_object_payload(+value_hash: String) -> Store.Op<&2, Maybe<&2, Value.Value>>:
465  do Store.Op<&2, Maybe<&2, Value.Value>>:
466    loaded_maybe : Maybe<&2, ContentObject.ContentObject> <- ContentObject.load_content_object(value_hash)
467    return payload_or_empty(loaded_maybe)
468
469# Tombstones read as absent; hashes load payloads.
470def decide_resolve_value(is_tombstone: Bool, +stored_value: String) -> Store.Op<&2, Maybe<&2, Value.Value>>:
471  match is_tombstone:
472    case True{}:
473      empty_value_maybe()
474    case False{}:
475      load_object_payload(stored_value)
476
477# Resolves one stored index value.
478def resolve_entry_value(+stored_value: String) -> Store.Op<&2, Maybe<&2, Value.Value>>:
479  decide_resolve_value(String.eq(stored_value, "TOMBSTONE"), stored_value)
480
481# Resolves a found chain entry to its payload.
482def resolve_found_entry(found_maybe: Maybe<&2, Reading.ChainEntry>) -> Store.Op<&2, Maybe<&2, Value.Value>>:
483  match found_maybe:
484    case None{}:
485      empty_value_maybe()
486    case Some{found_entry}:
487      resolve_entry_value(Reading.entry_value_of(found_entry))
488
489# Reads a namespaced value at a commit.
490def read_value_at(+namespace_name: String, +record_identifier: String, +commit_identifier: String) -> Store.Op<&2, Maybe<&2, Value.Value>>:
491  do Store.Op<&2, Maybe<&2, Value.Value>>:
492    ancestor_chain : List<&2, String> <- walk_commit_chain(10000n, commit_identifier)
493    found_entry : Maybe<&2, Reading.ChainEntry> <- search_chain_entries(ancestor_chain, namespace_name, record_identifier)
494    resolved_value : Maybe<&2, Value.Value> <- resolve_found_entry(found_entry)
495    return resolved_value