diff --git a/LEARNING_LOG.md b/LEARNING_LOG.md index 2528eae..444cb94 100644 --- a/LEARNING_LOG.md +++ b/LEARNING_LOG.md @@ -1,10 +1,10 @@ # Learning log -Current card: M5.2 — complete tree builder done (next: validating tree reader back to a mapping) +Current card: M5.2 — builder done on structural types (next: validating tree reader back to a mapping) Open prior compatibility check: M1.2 / NSID-001; strict upstream agreement remains incomplete Current release gate: L passed — local learning server only; R/S/C/O not passed Last working checkpoint: M4.3 CID content hashing and immutable blocks verified; 207 unit tests, compiler and format pass; M5.1 MST key layers are next; NSID-001 remains open -Last updated: 2026-10-08 +Last updated: 2026-10-18 This is the project's progress file. Resume the current card and the next exact action below; do not skip a card whose required checks or learner exercise remain @@ -293,6 +293,15 @@ initial commit and has since been deleted; the real module is - Next exact action: learner implements `build_at_layer` (partition, `list.try_map` gaps through `build_gap`, first gap result → left link, zip the rest with kept entries into node triples, `node_block`, collect gap blocks plus own block) and `build_root` (empty → `node_block(None, [])`; otherwise annotate, max layer via `list.fold`/`int.max`, build there). - Learner asked for clarification on `list.zip`; agent explained the positional correspondence (gap 0 → `l`, gap i → kept entry i−1's `t`) and the zipper pairing, noting a hand-rolled lockstep recursion is equivalent. Learner then wired partition + `try_map` + zip + triple reshaping correctly. Two iterations on the same type error shape: returning `node_block`'s single-block `#(Cid, BitArray)` where the whole-subtree `#(Cid, List(blocks))` contract was required — first in `build_at_layer` (fixed by flattening gap blocks and appending the own block), then in `build_root`'s empty branch (wrapping the single empty block in a list). Lesson recorded: `node_block` speaks one node, the build functions speak the whole subtree; the signatures caught both. Learner's `[left, ..rights]` match carries an explicit invariant panic for the impossible empty-gaps case. Final code: mutual recursion `build_at_layer`/`build_gap`, all steps correct. Removed the temporary `TreeBuildPending` variant and obsolete exercise comments; runner is unconditional and `mst_build_tree_test` joins default discovery. Final verification: explicit runner (independent root CID and five-block CID sequence match), `gleam check` (no warnings), `gleam test` (220 passed, no failures), and `gleam format --check` pass. - Next exact action: M5.2 reader side — a validating traversal that walks a complete tree from blocks back to a key→record-CID mapping, checking prefix reconstruction, link consistency and block presence; then pure lookup/create/replace/delete through the mapping-to-tree interface, before M5.3 fixture and differential coverage. + +## Tuple-to-record refactor (detour between M5.2 build and reader) + +- Learner proposed the detour after noticing tuple proliferation (`#(String, Cid, Int)` vs `#(String, Cid, Option(Cid))` distinguished only by position, and the `e.0.0`-style coordinate access) and asked about the panic smell. Agreed scope: the real win is making the "one more gap than entries" invariant structural, not cosmetics. New types in `src/pds/repo/mst.gleam`: `RecordEntry(key, value)`, `LayeredEntry(key, value, layer)`, `LinkedEntry(key, value, right)`, `Block(cid, bytes)`, `BuiltTree(root, blocks)`, and `Partition(left_gap, kept: List(KeptEntry))` with `KeptEntry(entry, right_gap)` — each kept entry carries its right gap, eliminating the zip and the unreachable panic in `build_at_layer`. +- Agent did the type declarations and mechanical conversions (leaf builders take `LinkedEntry`, `node_block` returns `Block`, `build_root` fully converted, `build_gap` destructures `BuiltTree`), and updated all seven MST runners to the new constructors with unchanged assertion semantics. The five completed runners pass unchanged-in-substance, including both independently computed node CIDs and the empty-node bytes — the mechanical conversion is proven behavior-preserving. +- Learner's exercise: rewrite the `partition_at_layer` walk for the new shape (current gap + completed kept + one pending kept entry awaiting its right gap; a kept entry closes the current gap as the pending entry's right gap or as `left_gap`) and simplify `build_at_layer` (no zip, no panic). Pending stubs: `partition_at_layer` returns the sentinel `Partition(left_gap: [], kept: [])` (detected by the runner on input `[a]@0`, whose correct answer differs); `build_at_layer` returns `Error(RefactorPending)` (temporary `NodeError` variant). Scaffold verification: `gleam check` passes with expected unused-argument warnings on both stubs; `gleam test` (218 passed, no failures — the two pending runners are out of default discovery until completed); `gleam format --check` passes. +- Next exact action: learner implements the new `partition_at_layer` walk and `build_at_layer`; then agent removes `RefactorPending`, makes both runners unconditional, and returns them to default discovery. +- After a few days away, the learner asked the agent to complete the exercise (delegation at learner request, per the established pattern). Reviewed their partial work en route: the recursive-walk signature needed the remaining input, `target_layer`, `List(KeptEntry)` for completed, and a `left` accumulator; their first terminal branch dropped the final gap and mixed reversal conventions; their two-way `pending` split was right after fixing a dropped `Some(entry)`. Final code: terminal `[]` branch closes `current` as the pending entry's right gap (or as `left_gap` when nothing was kept, using `option.unwrap(left, [])` for the structurally-impossible `Some/None` combination); kept-entry branches unchanged from the learner's corrected version; below-target branch prepends to `current`. `build_at_layer`: partition, `build_gap` the left gap, `list.try_map` kept entries into `LinkedEntry` + blocks, `node_block`, `BuiltTree` with left blocks, right-gap blocks in entry order, own block last — no zip, no panic. Removed `RefactorPending` and obsolete exercise comments; both runners unconditional and back in default discovery. Final verification: both explicit runners, `gleam check` (no warnings), `gleam test` (220 passed, no failures), and `gleam format --check` pass. The independent root CID and five-block CID sequence pass unchanged under the structural types, proving the refactor behavior-preserving. +- Next exact action: M5.2 reader side — a validating traversal that walks a complete tree from blocks back to a `RecordEntry` mapping, checking prefix reconstruction, link consistency and block presence. - Remaining boundary requirements: record/map-key/reserved-field rules, constructed-value encoder budgets, and actual official fixture coverage. Existing numeric, bytes, syntax duplicate-key and parser-budget checks are verified separately. No M4.1 completion or codec interoperability claim. Continue tracking strict NSID-001 independently; do not modify or skip its fixture. ## M1.1 checkpoint evidence diff --git a/src/pds/repo/mst.gleam b/src/pds/repo/mst.gleam index 7aaa723..77b04f3 100644 --- a/src/pds/repo/mst.gleam +++ b/src/pds/repo/mst.gleam @@ -19,14 +19,57 @@ pub type LeafError { DuplicateKey } -/// Failures when turning entries into a complete encoded node block. Each -/// constructor wraps the failing stage's own error type. +/// Failures when turning entries into a complete encoded node block or tree. +/// Each constructor wraps the failing stage's own error type. pub type NodeError { InvalidEntries(LeafError) EncodingFailed(cbor.EncodeError) CidFailed(cid.CidError) } +/// A raw record mapping entry: a repository path and its record's CID. +pub type RecordEntry { + RecordEntry(key: String, value: cid.Cid) +} + +/// An entry annotated with its key's MST layer. +pub type LayeredEntry { + LayeredEntry(key: String, value: cid.Cid, layer: Int) +} + +/// An entry placed in a node: its key, record CID, and the optional subtree +/// link holding keys that sort after this entry's key but before the next +/// entry (the wire `t` field). +pub type LinkedEntry { + LinkedEntry(key: String, value: cid.Cid, right: Option(cid.Cid)) +} + +/// A kept entry paired with the gap that sorts after it, so the "one more +/// gap than entries" rule is structural instead of implied by list lengths. +pub type KeptEntry { + KeptEntry(entry: LayeredEntry, right_gap: List(LayeredEntry)) +} + +/// An entry list split at a target layer. `left_gap` holds the entries +/// sorting before the first kept entry (the input for the node's `l` +/// subtree); each kept entry carries the gap sorting after it (the input for +/// its `t` subtree). Gaps may be empty; an empty gap means no subtree on that +/// side. +pub type Partition { + Partition(left_gap: List(LayeredEntry), kept: List(KeptEntry)) +} + +/// A content block: its CID and exact encoded bytes. +pub type Block { + Block(cid: cid.Cid, bytes: BitArray) +} + +/// A built subtree: the root block's CID and all blocks, children first and +/// the own block last. +pub type BuiltTree { + BuiltTree(root: cid.Cid, blocks: List(Block)) +} + pub fn layer_from_hash(hash: BitArray) -> Int { case hash { <<>> -> 0 @@ -84,7 +127,7 @@ fn check_key_characters(data: BitArray) -> Result(Nil, LeafError) { if // A-Z byte >= 65 && byte <= 90 - // a-z + // a-z || byte >= 97 && byte <= 122 // 0-9 || byte >= 48 && byte <= 57 @@ -104,19 +147,15 @@ fn check_key_characters(data: BitArray) -> Result(Nil, LeafError) { } } -pub fn first_leaf_value( - key: String, - value: cid.Cid, - right: Option(cid.Cid), -) -> Result(model.Value, LeafError) { - let key_bytes = bit_array.from_string(key) - use _ <- result.try(validate_key(key)) +pub fn first_leaf_value(entry: LinkedEntry) -> Result(model.Value, LeafError) { + let key_bytes = bit_array.from_string(entry.key) + use _ <- result.try(validate_key(entry.key)) Ok( model.Object([ #("p", model.Integer(0)), #("k", model.Bytes(key_bytes)), - #("v", model.Link(value)), - #("t", link_or_null(right)), + #("v", model.Link(entry.value)), + #("t", link_or_null(entry.right)), ]), ) } @@ -161,33 +200,31 @@ fn drop_bytes(bytes: BitArray, count: Int) -> BitArray { pub fn compressed_leaf_value( previous_key: String, - key: String, - value: cid.Cid, - right: Option(cid.Cid), + entry: LinkedEntry, ) -> Result(model.Value, LeafError) { - use _ <- result.try(validate_key(key)) + use _ <- result.try(validate_key(entry.key)) let prev_bytes = bit_array.from_string(previous_key) - let key_bytes = bit_array.from_string(key) + let key_bytes = bit_array.from_string(entry.key) let prefix = common_prefix_length(prev_bytes, key_bytes) let suffix = drop_bytes(key_bytes, prefix) Ok( model.Object([ #("p", model.Integer(prefix)), #("k", model.Bytes(suffix)), - #("v", model.Link(value)), - #("t", link_or_null(right)), + #("v", model.Link(entry.value)), + #("t", link_or_null(entry.right)), ]), ) } fn leaf_node_value_r( prev_key: Option(String), - entries: List(#(String, cid.Cid, Option(cid.Cid))), + entries: List(LinkedEntry), leaves: List(model.Value), left: Option(cid.Cid), ) -> Result(model.Value, LeafError) { case prev_key, entries { - _, [#(k1, _, _), #(k2, _, _), ..] if k1 == k2 -> Error(DuplicateKey) + _, [a, b, ..] if a.key == b.key -> Error(DuplicateKey) _, [] -> Ok( model.Object([ @@ -195,35 +232,33 @@ fn leaf_node_value_r( #("e", model.Array(list.reverse(leaves))), ]), ) - None, [#(k, v, right), ..rest] -> { - use leaf <- result.try(first_leaf_value(k, v, right)) - leaf_node_value_r(Some(k), rest, [leaf], left) + None, [entry, ..rest] -> { + use leaf <- result.try(first_leaf_value(entry)) + leaf_node_value_r(Some(entry.key), rest, [leaf], left) } - Some(p), [#(k, v, right), ..rest] -> { - use leaf <- result.try(compressed_leaf_value(p, k, v, right)) - leaf_node_value_r(Some(k), rest, [leaf, ..leaves], left) + Some(p), [entry, ..rest] -> { + use leaf <- result.try(compressed_leaf_value(p, entry)) + leaf_node_value_r(Some(entry.key), rest, [leaf, ..leaves], left) } } } /// Builds the wire Value for a single-level MST node: left subtree link `l` -/// plus prefix-compressed `e` entries. Each entry is a key, its record CID, -/// and the optional subtree link holding keys that sort after this entry's -/// key but before the next entry (`t`). Entries are validated, sorted by key, +/// plus prefix-compressed `e` entries. Entries are validated, sorted by key, /// and compressed against their predecessor; the first entry keeps prefix 0 /// and its full key. An empty entry list produces a node with only its `l` /// link — the intermediate empty node used when a layer has no keys of its /// own. Duplicate keys are invalid. pub fn leaf_node_value( left: Option(cid.Cid), - entries: List(#(String, cid.Cid, Option(cid.Cid))), + entries: List(LinkedEntry), ) -> Result(model.Value, LeafError) { case entries { [] -> Ok(empty_node_value(left)) _ -> leaf_node_value_r( None, - list.sort(entries, fn(a, b) { string.compare(a.0, b.0) }), + list.sort(entries, fn(a, b) { string.compare(a.key, b.key) }), [], left, ) @@ -231,73 +266,78 @@ pub fn leaf_node_value( } /// Encodes a single-level node as deterministic DAG-CBOR and derives its -/// content CID. Returns the CID paired with the exact block bytes; storing or -/// transmitting the bytes is a later storage concern. +/// content CID. Returns the block; storing or transmitting its bytes is a +/// later storage concern. pub fn node_block( left: Option(cid.Cid), - entries: List(#(String, cid.Cid, Option(cid.Cid))), -) -> Result(#(cid.Cid, BitArray), NodeError) { + entries: List(LinkedEntry), +) -> Result(Block, NodeError) { use leaf <- result.try(result.map_error( leaf_node_value(left, entries), InvalidEntries, )) use enc <- result.try(result.map_error(cbor.encode(leaf), EncodingFailed)) - use cid <- result.try(result.map_error(cid.for_dag_cbor(enc), CidFailed)) - Ok(#(cid, enc)) -} - -/// An entry list split at a target layer. `here` holds the entries whose key -/// layer equals the target, in ascending key order; they become this node's -/// own entries. `gaps` groups the remaining (lower-layer) entries into the -/// sorted gaps around them: gap 0 sorts before the first kept entry and -/// becomes the input for the node's `l` subtree; gap i > 0 sorts after kept -/// entry i - 1 and becomes the input for that entry's `t` subtree. There is -/// always exactly one more gap than kept entries, and gaps may be empty. -pub type Partition { - Partition( - here: List(#(String, cid.Cid, Int)), - gaps: List(List(#(String, cid.Cid, Int))), - ) + use block_cid <- result.try(result.map_error(cid.for_dag_cbor(enc), CidFailed)) + Ok(Block(block_cid, enc)) } /// Annotates each entry with its key's MST layer. The tree builder does this /// once so partitioning never re-hashes a key. -pub fn annotate_layers( - entries: List(#(String, cid.Cid)), -) -> List(#(String, cid.Cid, Int)) { - list.map(entries, fn(entry) { #(entry.0, entry.1, layer_for_key(entry.0)) }) +pub fn annotate_layers(entries: List(RecordEntry)) -> List(LayeredEntry) { + list.map(entries, fn(entry) { + LayeredEntry(entry.key, entry.value, layer_for_key(entry.key)) + }) } fn partition_at_layer_r( - entries: List(#(String, cid.Cid, Int)), + entries: List(LayeredEntry), target_layer: Int, - current: List(#(String, cid.Cid, Int)), - completed: List(List(#(String, cid.Cid, Int))), - kept: List(#(String, cid.Cid, Int)), + current: List(LayeredEntry), + pending: Option(LayeredEntry), + completed: List(KeptEntry), + left: Option(List(LayeredEntry)), ) -> Partition { case entries { [] -> - Partition( - list.reverse(kept), - list.reverse([list.reverse(current), ..completed]), - ) - [#(k, c, l), ..rest] -> - case target_layer == l { - True -> + case pending { + None -> Partition(left_gap: list.reverse(current), kept: []) + Some(entry) -> + Partition( + left_gap: option.unwrap(left, []), + kept: list.reverse([ + KeptEntry(entry, list.reverse(current)), + ..completed + ]), + ) + } + [entry, ..rest] -> + case target_layer == entry.layer, pending { + True, None -> + partition_at_layer_r( + rest, + target_layer, + [], + Some(entry), + completed, + Some(list.reverse(current)), + ) + True, Some(p) -> partition_at_layer_r( rest, target_layer, [], - [list.reverse(current), ..completed], - [#(k, c, l), ..kept], + Some(entry), + [KeptEntry(p, list.reverse(current)), ..completed], + left, ) - False -> + False, _ -> partition_at_layer_r( rest, target_layer, - [#(k, c, l), ..current], + [entry, ..current], + pending, completed, - kept, + left, ) } } @@ -305,15 +345,16 @@ fn partition_at_layer_r( /// Sorts annotated entries by key and splits them at `target_layer`. pub fn partition_at_layer( - entries: List(#(String, cid.Cid, Int)), + entries: List(LayeredEntry), target_layer: Int, ) -> Partition { partition_at_layer_r( - list.sort(entries, fn(a, b) { string.compare(a.0, b.0) }), + list.sort(entries, fn(a, b) { string.compare(a.key, b.key) }), target_layer, [], + None, [], - [], + None, ) } @@ -323,17 +364,14 @@ pub fn partition_at_layer( /// recursion with `build_at_layer` — a gap builds a node, whose gaps build /// nodes, and so on down to layer 0. fn build_gap( - gap: List(#(String, cid.Cid, Int)), + gap: List(LayeredEntry), layer: Int, -) -> Result(#(Option(cid.Cid), List(#(cid.Cid, BitArray))), NodeError) { +) -> Result(#(Option(cid.Cid), List(Block)), NodeError) { case gap { [] -> Ok(#(None, [])) _ -> { - use #(child_cid, child_blocks) <- result.try(build_at_layer( - gap, - layer - 1, - )) - Ok(#(Some(child_cid), child_blocks)) + use built <- result.try(build_at_layer(gap, layer - 1)) + Ok(#(Some(built.root), built.blocks)) } } } @@ -341,47 +379,44 @@ fn build_gap( /// Builds the node at `layer` from annotated entries (all with key layer at /// most `layer`), returning its root CID plus every block in its subtree. fn build_at_layer( - entries: List(#(String, cid.Cid, Int)), + entries: List(LayeredEntry), layer: Int, -) -> Result(#(cid.Cid, List(#(cid.Cid, BitArray))), NodeError) { - let partition = partition_at_layer(entries, layer) - use built_gaps <- result.try( - list.try_map(partition.gaps, fn(g) { build_gap(g, layer) }), - ) - case built_gaps { - [left, ..rights] -> { - use #(node_cid, node_bytes) <- result.try(node_block( - left.0, - list.map(list.zip(partition.here, rights), fn(e) { - #(e.0.0, e.0.1, e.1.0) - }), - )) - Ok(#( - node_cid, - list.map(built_gaps, fn(r) { r.1 }) - |> list.flatten - |> list.append([#(node_cid, node_bytes)]), +) -> Result(BuiltTree, NodeError) { + let Partition(left_gap: left_gap, kept: kept) = + partition_at_layer(entries, layer) + use #(left_link, left_blocks) <- result.try(build_gap(left_gap, layer)) + use built_kept <- result.try( + list.try_map(kept, fn(k) { + use #(right_link, right_blocks) <- result.try(build_gap( + k.right_gap, + layer, )) - } - _ -> panic as "Unreachable" - } + Ok(#(LinkedEntry(k.entry.key, k.entry.value, right_link), right_blocks)) + }), + ) + use own <- result.try(node_block( + left_link, + list.map(built_kept, fn(b) { b.0 }), + )) + let gap_blocks = + list.flatten([left_blocks, ..list.map(built_kept, fn(b) { b.1 })]) + Ok(BuiltTree(root: own.cid, blocks: list.append(gap_blocks, [own]))) } /// Builds a complete MST from a record mapping, returning the root CID and /// every tree block. The same mapping produces the same root regardless of /// input order. Blocks may repeat a CID when two subtrees are identical; the /// block store deduplicates. -pub fn build_root( - entries: List(#(String, cid.Cid)), -) -> Result(#(cid.Cid, List(#(cid.Cid, BitArray))), NodeError) { +pub fn build_root(entries: List(RecordEntry)) -> Result(BuiltTree, NodeError) { case entries { [] -> { - use #(node_cid, node_bytes) <- result.try(node_block(None, [])) - Ok(#(node_cid, [#(node_cid, node_bytes)])) + use block <- result.try(node_block(None, [])) + Ok(BuiltTree(root: block.cid, blocks: [block])) } - e -> { - let annotated = annotate_layers(e) - let max_layer = list.fold(annotated, 0, fn(acc, i) { int.max(acc, i.2) }) + _ -> { + let annotated = annotate_layers(entries) + let max_layer = + list.fold(annotated, 0, fn(acc, e) { int.max(acc, e.layer) }) build_at_layer(annotated, max_layer) } } diff --git a/test/pds/mst_build_tree_exercise.gleam b/test/pds/mst_build_tree_exercise.gleam index a10a0ca..510f58d 100644 --- a/test/pds/mst_build_tree_exercise.gleam +++ b/test/pds/mst_build_tree_exercise.gleam @@ -4,8 +4,8 @@ import gleam/option.{None} import pds/encoding/cid import pds/repo/mst -/// Completed learner runner, also in the default unit suite. The root CID and -/// the per-block CID list were computed independently of this codebase (a +/// Completed runner, also in the default unit suite. The root CID and the +/// per-block CID list were computed independently of this codebase (a /// from-scratch Python tree builder following the spec algorithm, /// sanity-checked against the known empty-tree root). pub fn main() -> Nil { @@ -15,29 +15,29 @@ pub fn main() -> Nil { cid.parse("bafyreie5737gdxlw5i64vzichcalba3z2v5n6icifvx5xytvske7mr3hpm") // The empty tree is the known empty node block. - let assert Ok(#(empty_root, empty_blocks)) = mst.build_root([]) - assert empty_root == cid_b - assert empty_blocks == [#(cid_b, <<162, 97, 101, 128, 97, 108, 246>>)] + let assert Ok(empty) = mst.build_root([]) + assert empty.root == cid_b + assert empty.blocks == [mst.Block(cid_b, <<162, 97, 101, 128, 97, 108, 246>>)] // A single layer-0 key is exactly the link-free node block. - let assert Ok(#(single_root, single_blocks)) = - mst.build_root([#("com.example.record/a", cid_a)]) - let assert Ok(#(direct_cid, direct_bytes)) = - mst.node_block(None, [#("com.example.record/a", cid_a, None)]) - assert single_root == direct_cid - assert single_blocks == [#(direct_cid, direct_bytes)] + let assert Ok(single) = + mst.build_root([mst.RecordEntry("com.example.record/a", cid_a)]) + let assert Ok(direct) = + mst.node_block(None, [mst.LinkedEntry("com.example.record/a", cid_a, None)]) + assert single.root == direct.cid + assert single.blocks == [direct] // The four-key tree spans three layers with an intermediate empty node. let tree = [ - #("com.example.record/a", cid_a), - #("com.example.record/ab", cid_b), - #("com.example.record/ba", cid_a), - #("com.example.record/c", cid_b), + mst.RecordEntry("com.example.record/a", cid_a), + mst.RecordEntry("com.example.record/ab", cid_b), + mst.RecordEntry("com.example.record/ba", cid_a), + mst.RecordEntry("com.example.record/c", cid_b), ] - let assert Ok(#(root, blocks)) = mst.build_root(tree) - assert root + let assert Ok(built) = mst.build_root(tree) + assert built.root == must_parse("bafyreibyf2t6vzo4xyc7hai5msev25yko5qamzvsyll72dplybbx6beu6y") - assert list.map(blocks, fn(b) { b.0 }) + assert list.map(built.blocks, fn(b) { b.cid }) == [ must_parse("bafyreifle2pvgqj5humebhdwpna6j56ycivz2i2exrx5exdtsrsvy65ryu"), must_parse("bafyreihoqgqhxcdfzzwk4ulaenqconxqnuwplmgupvzhgssuw3goip42vy"), @@ -47,22 +47,22 @@ pub fn main() -> Nil { ] // Insertion order does not change the tree. - let assert Ok(#(reordered_root, _)) = + let assert Ok(reordered) = mst.build_root([ - #("com.example.record/c", cid_b), - #("com.example.record/a", cid_a), - #("com.example.record/ba", cid_a), - #("com.example.record/ab", cid_b), + mst.RecordEntry("com.example.record/c", cid_b), + mst.RecordEntry("com.example.record/a", cid_a), + mst.RecordEntry("com.example.record/ba", cid_a), + mst.RecordEntry("com.example.record/ab", cid_b), ]) - assert reordered_root == root + assert reordered.root == built.root // Duplicate and invalid keys stay typed errors through the recursion. assert mst.build_root([ - #("com.example.record/a", cid_a), - #("com.example.record/a", cid_b), + mst.RecordEntry("com.example.record/a", cid_a), + mst.RecordEntry("com.example.record/a", cid_b), ]) == Error(mst.InvalidEntries(mst.DuplicateKey)) - assert mst.build_root([#("com.example.record/雪", cid_a)]) + assert mst.build_root([mst.RecordEntry("com.example.record/雪", cid_a)]) == Error(mst.InvalidEntries(mst.NonAsciiKey)) io.println("MST tree construction passed.") diff --git a/test/pds/mst_compressed_leaf_exercise.gleam b/test/pds/mst_compressed_leaf_exercise.gleam index 8b50566..38e1280 100644 --- a/test/pds/mst_compressed_leaf_exercise.gleam +++ b/test/pds/mst_compressed_leaf_exercise.gleam @@ -8,7 +8,7 @@ import pds/repo/mst pub fn main() -> Nil { let assert Ok(value_cid) = cid.parse("bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a") - assert mst.compressed_leaf_value("", "a/b", value_cid, None) + assert mst.compressed_leaf_value("", mst.LinkedEntry("a/b", value_cid, None)) == Ok( model.Object([ #("p", model.Integer(0)), @@ -19,9 +19,7 @@ pub fn main() -> Nil { ) assert mst.compressed_leaf_value( "bsky.posts/abcdefg", - "bsky.posts/abcdehi", - value_cid, - None, + mst.LinkedEntry("bsky.posts/abcdehi", value_cid, None), ) == Ok( model.Object([ @@ -31,7 +29,10 @@ pub fn main() -> Nil { #("t", model.Null), ]), ) - assert mst.compressed_leaf_value("a/b", "a/雪", value_cid, None) + assert mst.compressed_leaf_value( + "a/b", + mst.LinkedEntry("a/雪", value_cid, None), + ) == Error(mst.NonAsciiKey) io.println("MST compressed leaf construction passed.") } diff --git a/test/pds/mst_leaf_exercise.gleam b/test/pds/mst_leaf_exercise.gleam index 71b1f4d..f8b573c 100644 --- a/test/pds/mst_leaf_exercise.gleam +++ b/test/pds/mst_leaf_exercise.gleam @@ -8,7 +8,11 @@ import pds/repo/mst pub fn main() -> Nil { let assert Ok(value_cid) = cid.parse("bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a") - assert mst.first_leaf_value("app.bsky.feed.post/self", value_cid, None) + assert mst.first_leaf_value(mst.LinkedEntry( + "app.bsky.feed.post/self", + value_cid, + None, + )) == Ok( model.Object([ #("p", model.Integer(0)), @@ -17,8 +21,10 @@ pub fn main() -> Nil { #("t", model.Null), ]), ) - assert mst.first_leaf_value("", value_cid, None) != Error(mst.NonAsciiKey) - assert mst.first_leaf_value("a/雪", value_cid, None) == Error(mst.NonAsciiKey) + assert mst.first_leaf_value(mst.LinkedEntry("", value_cid, None)) + != Error(mst.NonAsciiKey) + assert mst.first_leaf_value(mst.LinkedEntry("a/雪", value_cid, None)) + == Error(mst.NonAsciiKey) io.println("First ASCII MST leaf construction passed.") } diff --git a/test/pds/mst_node_block_exercise.gleam b/test/pds/mst_node_block_exercise.gleam index 2e2d101..f6f128d 100644 --- a/test/pds/mst_node_block_exercise.gleam +++ b/test/pds/mst_node_block_exercise.gleam @@ -13,25 +13,27 @@ pub fn main() -> Nil { let assert Ok(cid_b) = cid.parse("bafyreie5737gdxlw5i64vzichcalba3z2v5n6icifvx5xytvske7mr3hpm") - let assert Ok(#(empty_cid, empty_bytes)) = mst.node_block(None, []) - assert empty_bytes == <<162, 97, 101, 128, 97, 108, 246>> - assert empty_cid == cid_b + let assert Ok(empty_block) = mst.node_block(None, []) + assert empty_block.bytes == <<162, 97, 101, 128, 97, 108, 246>> + assert empty_block.cid == cid_b - let assert Ok(#(node_cid, node_bytes)) = + let assert Ok(node) = mst.node_block(None, [ - #("com.example.record/b", cid_b, None), - #("com.example.record/a", cid_a, None), + mst.LinkedEntry("com.example.record/b", cid_b, None), + mst.LinkedEntry("com.example.record/a", cid_a, None), ]) - assert bit_array.byte_size(node_bytes) == 134 - assert node_cid + assert bit_array.byte_size(node.bytes) == 134 + assert node.cid == must_parse("bafyreifvm6svoq3hzvrrnhchtz7d6gsnj4j65lc7lbz5l6hstimmpdff74") - assert mst.node_block(None, [#("com.example.record/雪", cid_a, None)]) + assert mst.node_block(None, [ + mst.LinkedEntry("com.example.record/雪", cid_a, None), + ]) == Error(mst.InvalidEntries(mst.NonAsciiKey)) assert mst.node_block(None, [ - #("com.example.record/a", cid_a, None), - #("com.example.record/a", cid_b, None), + mst.LinkedEntry("com.example.record/a", cid_a, None), + mst.LinkedEntry("com.example.record/a", cid_b, None), ]) == Error(mst.InvalidEntries(mst.DuplicateKey)) diff --git a/test/pds/mst_node_exercise.gleam b/test/pds/mst_node_exercise.gleam index 949bb4d..fc7c980 100644 --- a/test/pds/mst_node_exercise.gleam +++ b/test/pds/mst_node_exercise.gleam @@ -27,24 +27,28 @@ pub fn main() -> Nil { assert mst.leaf_node_value(None, []) == Ok(mst.empty_node_value(None)) - assert mst.leaf_node_value(None, [#("com.example.record/a", cid_a, None)]) + assert mst.leaf_node_value(None, [ + mst.LinkedEntry("com.example.record/a", cid_a, None), + ]) == Ok(model.Object([#("l", model.Null), #("e", model.Array([leaf_a]))])) assert mst.leaf_node_value(None, [ - #("com.example.record/b", cid_b, None), - #("com.example.record/a", cid_a, None), + mst.LinkedEntry("com.example.record/b", cid_b, None), + mst.LinkedEntry("com.example.record/a", cid_a, None), ]) == Ok( model.Object([#("l", model.Null), #("e", model.Array([leaf_a, leaf_b]))]), ) assert mst.leaf_node_value(None, [ - #("com.example.record/a", cid_a, None), - #("com.example.record/a", cid_b, None), + mst.LinkedEntry("com.example.record/a", cid_a, None), + mst.LinkedEntry("com.example.record/a", cid_b, None), ]) == Error(mst.DuplicateKey) - assert mst.leaf_node_value(None, [#("com.example.record/雪", cid_a, None)]) + assert mst.leaf_node_value(None, [ + mst.LinkedEntry("com.example.record/雪", cid_a, None), + ]) == Error(mst.NonAsciiKey) io.println("MST single-level node construction passed.") diff --git a/test/pds/mst_partition_exercise.gleam b/test/pds/mst_partition_exercise.gleam index c530b46..50a9d28 100644 --- a/test/pds/mst_partition_exercise.gleam +++ b/test/pds/mst_partition_exercise.gleam @@ -2,8 +2,8 @@ import gleam/io import pds/encoding/cid import pds/repo/mst -/// Completed learner runner, also in the default unit suite. Key layers are -/// the independently computed known vectors: a/0, b/1, ab/2, ba/1, c/0, zz/1, +/// Completed runner, also in the default unit suite. Key layers are the +/// independently computed known vectors: a/0, b/1, ab/2, ba/1, c/0, zz/1, /// app.bsky.feed.post/self/2. Note "app..." sorts before "com..." bytewise. pub fn main() -> Nil { let assert Ok(cid_a) = @@ -11,44 +11,49 @@ pub fn main() -> Nil { let assert Ok(cid_b) = cid.parse("bafyreie5737gdxlw5i64vzichcalba3z2v5n6icifvx5xytvske7mr3hpm") - let a = #("com.example.record/a", cid_a, 0) - let ab = #("com.example.record/ab", cid_b, 2) - let ba = #("com.example.record/ba", cid_a, 1) - let c = #("com.example.record/c", cid_b, 0) - let zz = #("com.example.record/zz", cid_a, 1) - let b = #("com.example.record/b", cid_b, 1) - let self_ = #("app.bsky.feed.post/self", cid_a, 2) + let a = mst.LayeredEntry("com.example.record/a", cid_a, 0) + let ab = mst.LayeredEntry("com.example.record/ab", cid_b, 2) + let ba = mst.LayeredEntry("com.example.record/ba", cid_a, 1) + let c = mst.LayeredEntry("com.example.record/c", cid_b, 0) + let zz = mst.LayeredEntry("com.example.record/zz", cid_a, 1) + let b = mst.LayeredEntry("com.example.record/b", cid_b, 1) + let self_ = mst.LayeredEntry("app.bsky.feed.post/self", cid_a, 2) // The supplied annotate helper applies the known key layers. assert mst.annotate_layers([ - #("com.example.record/a", cid_a), - #("app.bsky.feed.post/self", cid_b), + mst.RecordEntry("com.example.record/a", cid_a), + mst.RecordEntry("app.bsky.feed.post/self", cid_b), ]) == [ - #("com.example.record/a", cid_a, 0), - #("app.bsky.feed.post/self", cid_b, 2), + mst.LayeredEntry("com.example.record/a", cid_a, 0), + mst.LayeredEntry("app.bsky.feed.post/self", cid_b, 2), ] - // Empty input: no kept entries, one empty gap. - assert mst.partition_at_layer([], 0) == mst.Partition([], [[]]) + // Empty input: no kept entries, empty left gap. + assert mst.partition_at_layer([], 0) == mst.Partition(left_gap: [], kept: []) // One kept entry with a nonempty gap on each side. let result = mst.partition_at_layer([a, ab, ba, c], 2) - assert result == mst.Partition([ab], [[a], [ba, c]]) + assert result + == mst.Partition(left_gap: [a], kept: [mst.KeptEntry(ab, [ba, c])]) // Unsorted input produces the same partition. assert mst.partition_at_layer([c, ab, a, ba], 2) == result - // Two kept entries: exactly three gaps, the trailing one empty. + // Two kept entries; the second's right gap is empty. assert mst.partition_at_layer([a, ba, c, zz], 1) - == mst.Partition([ba, zz], [[a], [c], []]) + == mst.Partition(left_gap: [a], kept: [ + mst.KeptEntry(ba, [c]), + mst.KeptEntry(zz, []), + ]) - // Every entry below the target: all fall into the single gap. - assert mst.partition_at_layer([a, b], 2) == mst.Partition([], [[a, b]]) + // Every entry below the target: all fall into the left gap. + assert mst.partition_at_layer([a, b], 2) + == mst.Partition(left_gap: [a, b], kept: []) - // The highest-layer key sorts first here, so its left gap is empty. + // The highest-layer key sorts first here, so the left gap is empty. assert mst.partition_at_layer([a, b, self_], 2) - == mst.Partition([self_], [[], [a, b]]) + == mst.Partition(left_gap: [], kept: [mst.KeptEntry(self_, [a, b])]) io.println("MST layer partitioning passed.") } diff --git a/test/pds/mst_subtree_link_exercise.gleam b/test/pds/mst_subtree_link_exercise.gleam index d1d4ea6..420d633 100644 --- a/test/pds/mst_subtree_link_exercise.gleam +++ b/test/pds/mst_subtree_link_exercise.gleam @@ -20,7 +20,7 @@ pub fn main() -> Nil { // A single entry carrying a right subtree link. assert mst.leaf_node_value(None, [ - #("com.example.record/a", cid_a, Some(cid_b)), + mst.LinkedEntry("com.example.record/a", cid_a, Some(cid_b)), ]) == Ok( model.Object([ @@ -41,8 +41,8 @@ pub fn main() -> Nil { // A left link plus two sorted entries; the first carries a right link. assert mst.leaf_node_value(Some(cid_b), [ - #("com.example.record/b", cid_b, None), - #("com.example.record/a", cid_a, Some(cid_b)), + mst.LinkedEntry("com.example.record/b", cid_b, None), + mst.LinkedEntry("com.example.record/a", cid_a, Some(cid_b)), ]) == Ok( model.Object([ @@ -68,13 +68,13 @@ pub fn main() -> Nil { ) // A full block with links: the CID was computed independently. - let assert Ok(#(linked_cid, linked_bytes)) = + let assert Ok(linked) = mst.node_block(Some(cid_b), [ - #("com.example.record/a", cid_a, None), - #("com.example.record/b", cid_b, Some(cid_a)), + mst.LinkedEntry("com.example.record/a", cid_a, None), + mst.LinkedEntry("com.example.record/b", cid_b, Some(cid_a)), ]) - assert bit_array.byte_size(linked_bytes) == 214 - assert linked_cid + assert bit_array.byte_size(linked.bytes) == 214 + assert linked.cid == must_parse("bafyreihyyxi4wpzoxovjl256mkniqpmeik5ipvodlkxrxvf2dfspugtjze") io.println("MST subtree links passed.")