diff --git a/.gitignore b/.gitignore index 1420a51..860c96c 100644 --- a/.gitignore +++ b/.gitignore @@ -90,8 +90,8 @@ temp/ *.duckdb # Dependency lock files (optional) -poetry.lock -uv.lock +#poetry.lock +#uv.lock # Additional files and directories to ignore (put below) *_output.txt diff --git a/AGENTS.md b/AGENTS.md index 734247d..20e703e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,20 +26,24 @@ Priorities, in order: - Use Oxford commas in inline lists: "a, b, and c" not "a, b, c". - Do not use em dashes. Restructure the sentence, or use a colon or semicolon instead. -- Avoid colorful adjectives and adverbs. Write "TCP proxy" not "lightweight TCP proxy", "scoring components" not "transparent scoring components". -- Use noun phrases for checklist items, not imperative verbs. Write "redundant index detection" not "detect redundant indexes". -- Headings in Markdown files must be in the title case: "Build from Source" not "Build from source". Minor words (a, an, the, and, but, or, for, in, - on, at, to, by, of, is, are, was, were, be) stay lowercase unless they are the first word. +- Avoid colorful adjectives and adverbs. Write "adjacency query" not "blazing adjacency query". +- Prefer noun phrases for checklist items over imperative verbs. Write "temp directory teardown" not "tear down the temp directory". +- Headings in Markdown files must be in title case: "Build from Source" not "Build from source". Minor words (a, an, the, and, but, or, for, in, on, + at, to, by, of) stay lowercase unless they are the first word. +- Write correct and complete sentences. +- Avoid made-up words, abbreviations, and colons in the middle of sentences. ## Repository Layout - `src/lib.zig`: Public API entry point. Re-exports `SortedSet`, `RedBlackTreeSet`, `BTreeMap`, `SkipListMap`, `TrieMap`, and `CartesianTreeMap`. - `src/ordered/sorted_set.zig`: `SortedSet` (insertion-sorted `std.ArrayList` backed by a linear scan for insert and removal). -- `src/ordered/red_black_tree_set.zig`: `RedBlackTreeSet` (self-balancing BST; takes an explicit three-way comparison function, consistent with the other generic-key containers). +- `src/ordered/red_black_tree_set.zig`: `RedBlackTreeSet` (self-balancing BST; takes an explicit three-way comparison function, consistent with the + other generic-key containers). - `src/ordered/btree_map.zig`: `BTreeMap` (cache-friendly B-tree with configurable branching factor). - `src/ordered/skip_list_map.zig`: `SkipListMap` (probabilistic skip list with a per-instance PRNG). - `src/ordered/trie_map.zig`: `TrieMap` (prefix tree, specialised for `[]const u8` keys). -- `src/ordered/cartesian_tree_map.zig`: `CartesianTreeMap` (treap combining BST ordering with max-heap priorities; takes an explicit key-comparison function). +- `src/ordered/cartesian_tree_map.zig`: `CartesianTreeMap` (treap combining BST ordering with max-heap priorities; takes an explicit key-comparison + function). - `examples/`: Self-contained example programs (`e1_btree_map.zig` through `e6_cartesian_tree_map.zig`) built as executables via `build.zig`. - `benches/`: Benchmark programs (`b1_btree_map.zig` through `b6_cartesian_tree_map.zig`) built in `ReleaseFast`. - `benches/util/timer.zig`: Internal compatibility shim for the removed `std.time.Timer`, backed by `std.Io.Timestamp`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/README.md b/README.md index 7c4b08c..d242586 100644 --- a/README.md +++ b/README.md @@ -169,10 +169,6 @@ pub fn main() !void { You can find the API documentation for the latest release of Ordered [here](https://CogitatorTech.github.io/ordered/). -Alternatively, you can use the `make docs` command to generate the documentation for the current version of Ordered. -This will generate HTML documentation in the `docs/api` directory, which you can serve locally with `make serve-docs` -and view in a web browser. - ### Examples Check out the [examples](examples) directory for example usages of Ordered. diff --git a/src/ordered/btree_map.zig b/src/ordered/btree_map.zig index cf04c69..9ca2b3f 100644 --- a/src/ordered/btree_map.zig +++ b/src/ordered/btree_map.zig @@ -313,8 +313,17 @@ pub fn BTreeMap( while (i > 0 and compare(key, node.keys[i - 1]) == .lt) : (i -= 1) {} if (node.children[i].?.len == BRANCHING_FACTOR - 1) { try self.splitChild(node, i); - if (compare(node.keys[i], key) == .lt) { - i += 1; + // The split promotes a median key into this node at index `i`. + // If it equals `key`, the key now lives here: update in + // place instead of descending, which would otherwise insert + // a duplicate into the left half. + switch (compare(key, node.keys[i])) { + .eq => { + node.values[i] = value; + return false; + }, + .gt => i += 1, + .lt => {}, } } return try self.insertNonFull(node.children[i].?, key, value); @@ -637,6 +646,74 @@ fn i32Compare(lhs: i32, rhs: i32) std.math.Order { return std.math.order(lhs, rhs); } +const MapOracle = @import("oracle.zig").MapOracle; + +fn btreeDifferential(comptime B: u16) !void { + const allocator = std.testing.allocator; + var map = BTreeMap(i32, i32, i32Compare, B).init(allocator); + defer map.deinit(); + + var oracle: MapOracle(i32, i32, i32Compare) = .{}; + defer oracle.deinit(allocator); + + // Fixed, per-branching-factor seed keeps the sequence deterministic. + var prng = std.Random.DefaultPrng.init(0x1111_2222_3333_0000 + @as(u64, B)); + const random = prng.random(); + + const operations = 3000; + const key_space: u32 = 200; + + var op: usize = 0; + while (op < operations) : (op += 1) { + const key: i32 = @intCast(random.uintLessThan(u32, key_space)); + // A monotonically increasing value lets us detect a stale value left + // behind by a buggy update or remove. + const value: i32 = @intCast(op); + + if (random.uintLessThan(u32, 3) == 0) { + const removed = map.remove(key); + const oracle_removed = oracle.remove(key); + try std.testing.expectEqual(oracle_removed != null, removed != null); + if (oracle_removed) |ov| try std.testing.expectEqual(ov, removed.?); + } else { + try map.put(key, value); + try oracle.put(allocator, key, value); + } + + try std.testing.expectEqual(oracle.count(), map.count()); + if (oracle.get(key)) |ov| { + try std.testing.expectEqual(ov, map.get(key).?.*); + } else { + try std.testing.expect(map.get(key) == null); + } + + var iter = try map.iterator(); + defer iter.deinit(); + var idx: usize = 0; + while (try iter.next()) |entry| : (idx += 1) { + try std.testing.expect(idx < oracle.entries.items.len); + try std.testing.expectEqual(oracle.entries.items[idx].key, entry.key); + try std.testing.expectEqual(oracle.entries.items[idx].value, entry.value); + } + try std.testing.expectEqual(oracle.entries.items.len, idx); + + if (op % 100 == 0) { + var k: i32 = 0; + while (k < @as(i32, @intCast(key_space))) : (k += 1) { + try std.testing.expectEqual(oracle.contains(k), map.contains(k)); + } + } + } +} + +test "BTreeMap: differential test against sorted-array oracle" { + // Cover even and odd branching factors; odd B has historically exercised + // distinct merge and split paths. + inline for ([_]u16{ 4, 5, 6, 7 }) |B| { + try btreeDifferential(B); + } +} + test "BTreeMap: put, get, and delete" { const allocator = std.testing.allocator; const B = 4; @@ -859,3 +936,34 @@ test "regression: BTreeMap sequential delete with odd B stays valid" { try std.testing.expectEqual(@as(i32, i * 2), map.get(i).?.*); } } + +test "regression: BTreeMap put of a split median does not duplicate the key" { + // Bug found by the differential oracle: inserting a key equal to the median + // promoted during a child split descended into the left half and inserted a + // second copy, inflating the count and corrupting search. With B=4 the keys + // 1..7 build a tree whose right leaf is the full node [5, 6, 7], whose median + // is 6. Re-inserting 6 must update in place rather than add a duplicate. + const allocator = std.testing.allocator; + var map = BTreeMap(i32, i32, i32Compare, 4).init(allocator); + defer map.deinit(); + + var k: i32 = 1; + while (k <= 7) : (k += 1) try map.put(k, k); + try std.testing.expectEqual(@as(usize, 7), map.count()); + + try map.put(6, 600); + try std.testing.expectEqual(@as(usize, 7), map.count()); + try std.testing.expectEqual(@as(i32, 600), map.get(6).?.*); + + // Iteration must be strictly increasing (a duplicated 6 would break this) + // and visit exactly seven keys. + var iter = try map.iterator(); + defer iter.deinit(); + var prev: ?i32 = null; + var n: usize = 0; + while (try iter.next()) |entry| : (n += 1) { + if (prev) |p| try std.testing.expect(entry.key > p); + prev = entry.key; + } + try std.testing.expectEqual(@as(usize, 7), n); +} diff --git a/src/ordered/cartesian_tree_map.zig b/src/ordered/cartesian_tree_map.zig index d7e7568..9501f10 100644 --- a/src/ordered/cartesian_tree_map.zig +++ b/src/ordered/cartesian_tree_map.zig @@ -110,15 +110,21 @@ pub fn CartesianTreeMap( /// Inserts a key-value pair with a random priority. /// /// Uses per-instance PRNG priorities to ensure expected O(log n) performance. - /// If the key already exists, updates its value and priority. + /// If the key already exists, updates its value in place; the node keeps + /// its current priority, so the tree shape (and balance) is unchanged. /// /// Time complexity: O(log n) expected /// /// ## Errors /// Returns `error.OutOfMemory` if node allocation fails. pub fn put(self: *Self, key: K, value: V) !void { - const priority = self.prng.random().int(u32); - try self.putWithPriority(key, value, priority); + // A value-only update keeps the existing priority: re-randomising it + // on every update would churn the tree structure for no benefit. + if (self.findNode(key)) |existing| { + existing.value = value; + return; + } + try self.insertNew(key, value, self.prng.random().int(u32)); } /// Inserts a key-value pair with an explicit priority. @@ -134,6 +140,20 @@ pub fn CartesianTreeMap( /// ## Errors /// Returns `error.OutOfMemory` if node allocation fails. pub fn putWithPriority(self: *Self, key: K, value: V, priority: u32) !void { + // To honor the explicit priority on an update, remove the existing + // node and reinsert it. Reinsertion positions the node by its new + // priority and preserves the max-heap invariant, whereas writing the + // priority in place would leave a node out of heap order relative to + // its parent or children. + if (self.contains(key)) { + _ = self.remove(key); + } + try self.insertNew(key, value, priority); + } + + /// Inserts a key known not to be present. Callers must ensure the key is + /// absent so the insertion never produces a duplicate. + fn insertNew(self: *Self, key: K, value: V, priority: u32) !void { const new_node = try self.allocator.create(Node); new_node.* = Node.init(key, value, priority); @@ -146,6 +166,18 @@ pub fn CartesianTreeMap( self.root = self.insertNode(self.root, new_node); } + fn findNode(self: *Self, key: K) ?*Node { + var current = self.root; + while (current) |node| { + switch (compare(key, node.key)) { + .eq => return node, + .lt => current = node.left, + .gt => current = node.right, + } + } + return null; + } + fn insertNode(self: *Self, root: ?*Node, new_node: *Node) ?*Node { if (root == null) { self.len += 1; @@ -378,6 +410,127 @@ fn i32Compare(lhs: i32, rhs: i32) std.math.Order { return std.math.order(lhs, rhs); } +const MapOracle = @import("oracle.zig").MapOracle; + +const TreapI32 = CartesianTreeMap(i32, i32, i32Compare); + +/// Recursively asserts the two invariants every treap node must satisfy: BST +/// ordering on keys and the max-heap property on priorities (a parent's +/// priority is greater than or equal to each child's). +fn expectTreapInvariants(node: ?*const TreapI32.Node) !void { + const n = node orelse return; + if (n.left) |l| { + try testing.expect(i32Compare(l.key, n.key) == .lt); + try testing.expect(l.priority <= n.priority); + try expectTreapInvariants(l); + } + if (n.right) |r| { + try testing.expect(i32Compare(r.key, n.key) == .gt); + try testing.expect(r.priority <= n.priority); + try expectTreapInvariants(r); + } +} + +test "CartesianTreeMap: differential test against sorted-array oracle" { + const allocator = testing.allocator; + var tree = TreapI32.init(allocator); + defer tree.deinit(); + + var oracle: MapOracle(i32, i32, i32Compare) = .{}; + defer oracle.deinit(allocator); + + // The treap seeds its own PRNG internally for priorities; this separate + // PRNG only drives the operation sequence and is fixed-seed for determinism. + var prng = std.Random.DefaultPrng.init(0xca27_e51a_7700_0000); + const random = prng.random(); + + const operations = 3000; + const key_space: u32 = 200; + + var op: usize = 0; + while (op < operations) : (op += 1) { + const key: i32 = @intCast(random.uintLessThan(u32, key_space)); + const value: i32 = @intCast(op); + + if (random.uintLessThan(u32, 3) == 0) { + const removed = tree.remove(key); + const oracle_removed = oracle.remove(key); + try testing.expectEqual(oracle_removed != null, removed != null); + if (oracle_removed) |ov| try testing.expectEqual(ov, removed.?); + } else { + try tree.put(key, value); + try oracle.put(allocator, key, value); + } + + try testing.expectEqual(oracle.count(), tree.count()); + if (oracle.get(key)) |ov| { + try testing.expectEqual(ov, tree.get(key).?); + } else { + try testing.expect(tree.get(key) == null); + } + + var iter = try tree.iterator(allocator); + defer iter.deinit(); + var idx: usize = 0; + while (try iter.next()) |entry| : (idx += 1) { + try testing.expect(idx < oracle.entries.items.len); + try testing.expectEqual(oracle.entries.items[idx].key, entry.key); + try testing.expectEqual(oracle.entries.items[idx].value, entry.value); + } + try testing.expectEqual(oracle.entries.items.len, idx); + + if (op % 100 == 0) { + var k: i32 = 0; + while (k < @as(i32, @intCast(key_space))) : (k += 1) { + try testing.expectEqual(oracle.contains(k), tree.contains(k)); + } + // Every put, remove, and value-update must leave the treap a valid + // max-heap on priorities and a valid BST on keys. + try expectTreapInvariants(tree.root); + } + } +} + +test "CartesianTreeMap: putWithPriority update preserves heap invariant" { + var tree = TreapI32.init(testing.allocator); + defer tree.deinit(); + + try tree.putWithPriority(5, 50, 10); + try tree.putWithPriority(3, 30, 20); + try tree.putWithPriority(8, 80, 5); + try tree.putWithPriority(1, 10, 15); + try tree.putWithPriority(7, 70, 25); + try expectTreapInvariants(tree.root); + + // Raising key 5's priority above all others must lift it to the root while + // keeping the structure a valid treap. + try tree.putWithPriority(5, 55, 100); + try testing.expectEqual(@as(usize, 5), tree.count()); + try testing.expectEqual(@as(i32, 55), tree.get(5).?); + try testing.expectEqual(@as(i32, 5), tree.root.?.key); + try testing.expectEqual(@as(u32, 100), tree.root.?.priority); + try expectTreapInvariants(tree.root); + + // Lowering it again must re-sink it without breaking the invariants, and the + // value update must persist. + try tree.putWithPriority(5, 555, 1); + try testing.expectEqual(@as(usize, 5), tree.count()); + try testing.expectEqual(@as(i32, 555), tree.get(5).?); + try expectTreapInvariants(tree.root); +} + +test "CartesianTreeMap: put updates value without changing priority" { + var tree = TreapI32.init(testing.allocator); + defer tree.deinit(); + + try tree.putWithPriority(10, 100, 42); + // A value-only `put` must leave the node's priority untouched. + try tree.put(10, 200); + try testing.expectEqual(@as(usize, 1), tree.count()); + try testing.expectEqual(@as(i32, 200), tree.get(10).?); + try testing.expectEqual(@as(u32, 42), tree.root.?.priority); +} + test "CartesianTreeMap basic operations" { var tree = CartesianTreeMap(i32, []const u8, i32Compare).init(testing.allocator); defer tree.deinit(); @@ -582,3 +735,32 @@ test "CartesianTreeMap: same priorities different keys" { try testing.expect(tree.contains(10)); try testing.expect(tree.contains(15)); } + +test "regression: CartesianTreeMap update with higher priority does not duplicate the key" { + // Bug found by the differential oracle: updating an existing key whose new + // priority exceeded an ancestor's caused `insertNode` to split and insert a + // second node with the same key. Key 5 sits below the higher-priority key + // 10; re-inserting 5 with a priority above 10 must update in place. + var tree = TreapI32.init(testing.allocator); + defer tree.deinit(); + + try tree.putWithPriority(10, 100, 50); + try tree.putWithPriority(5, 5, 10); // becomes 10's left child + try testing.expectEqual(@as(usize, 2), tree.count()); + + try tree.putWithPriority(5, 55, 100); // new priority exceeds the ancestor + try testing.expectEqual(@as(usize, 2), tree.count()); + try testing.expectEqual(@as(i32, 55), tree.get(5).?); + + // Exactly one node with key 5 remains, and the tree is still a valid treap. + var iter = try tree.iterator(testing.allocator); + defer iter.deinit(); + var fives: usize = 0; + var n: usize = 0; + while (try iter.next()) |entry| : (n += 1) { + if (entry.key == 5) fives += 1; + } + try testing.expectEqual(@as(usize, 1), fives); + try testing.expectEqual(@as(usize, 2), n); + try expectTreapInvariants(tree.root); +} diff --git a/src/ordered/oracle.zig b/src/ordered/oracle.zig new file mode 100644 index 0000000..d5be882 --- /dev/null +++ b/src/ordered/oracle.zig @@ -0,0 +1,122 @@ +//! Naive, obviously-correct reference containers used as differential-testing +//! oracles for the library's sets and maps. +//! +//! Each oracle keeps its elements in a sorted, duplicate-free `std.ArrayList` +//! via linear scan. The implementations are small enough to audit by +//! inspection, so when a real container and its oracle agree across a long +//! random sequence of operations (count, membership, value, removal success, +//! and in-order iteration), that agreement is strong evidence the container is +//! correct. This module is internal and used only by the inline `test` blocks +//! in the container modules; it is not part of the public API. + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Order = std.math.Order; + +/// A naive sorted, duplicate-free set model parameterised by element type and +/// the same three-way comparison function the container under test uses. +pub fn SetOracle(comptime T: type, comptime compare: fn (lhs: T, rhs: T) Order) type { + return struct { + const Self = @This(); + + items: std.ArrayList(T) = .empty, + + pub fn deinit(self: *Self, allocator: Allocator) void { + self.items.deinit(allocator); + } + + pub fn contains(self: *const Self, value: T) bool { + for (self.items.items) |v| { + if (compare(v, value) == .eq) return true; + } + return false; + } + + /// Inserts `value`, keeping the list sorted. A value that is already + /// present leaves the set structurally unchanged. + pub fn put(self: *Self, allocator: Allocator, value: T) !void { + var i: usize = 0; + while (i < self.items.items.len and compare(self.items.items[i], value) == .lt) : (i += 1) {} + if (i < self.items.items.len and compare(self.items.items[i], value) == .eq) return; + try self.items.insert(allocator, i, value); + } + + /// Removes `value` if present, returning whether it existed. + pub fn remove(self: *Self, value: T) bool { + for (self.items.items, 0..) |v, i| { + if (compare(v, value) == .eq) { + _ = self.items.orderedRemove(i); + return true; + } + } + return false; + } + + pub fn count(self: *const Self) usize { + return self.items.items.len; + } + }; +} + +/// A naive sorted, duplicate-free key-value map model parameterised by key +/// type, value type, and the container's key-comparison function. +pub fn MapOracle( + comptime K: type, + comptime V: type, + comptime compare: fn (lhs: K, rhs: K) Order, +) type { + return struct { + const Self = @This(); + + pub const Entry = struct { key: K, value: V }; + + entries: std.ArrayList(Entry) = .empty, + + pub fn deinit(self: *Self, allocator: Allocator) void { + self.entries.deinit(allocator); + } + + fn find(self: *const Self, key: K) ?usize { + for (self.entries.items, 0..) |e, i| { + if (compare(e.key, key) == .eq) return i; + } + return null; + } + + pub fn contains(self: *const Self, key: K) bool { + return self.find(key) != null; + } + + pub fn get(self: *const Self, key: K) ?V { + if (self.find(key)) |i| return self.entries.items[i].value; + return null; + } + + /// Inserts the pair, keeping keys sorted. An existing key has its value + /// updated in place, mirroring the containers' upsert semantics. + pub fn put(self: *Self, allocator: Allocator, key: K, value: V) !void { + var i: usize = 0; + while (i < self.entries.items.len and compare(self.entries.items[i].key, key) == .lt) : (i += 1) {} + if (i < self.entries.items.len and compare(self.entries.items[i].key, key) == .eq) { + self.entries.items[i].value = value; + return; + } + try self.entries.insert(allocator, i, .{ .key = key, .value = value }); + } + + /// Removes `key` if present, returning its value. + pub fn remove(self: *Self, key: K) ?V { + for (self.entries.items, 0..) |e, i| { + if (compare(e.key, key) == .eq) { + _ = self.entries.orderedRemove(i); + return e.value; + } + } + return null; + } + + pub fn count(self: *const Self) usize { + return self.entries.items.len; + } + }; +} diff --git a/src/ordered/red_black_tree_set.zig b/src/ordered/red_black_tree_set.zig index 5bdcab1..0a8a130 100644 --- a/src/ordered/red_black_tree_set.zig +++ b/src/ordered/red_black_tree_set.zig @@ -249,115 +249,120 @@ pub fn RedBlackTreeSet( } fn removeNode(self: *Self, node: *Node) void { - var deleted_node = node; - var deleted_color = deleted_node.color; + // `replacement` is the node (possibly null) that moves into the + // spliced-out position; `replacement_parent` records its parent + // explicitly. The parent is needed because, without a NIL sentinel, + // a null replacement carries no parent pointer of its own, and + // `fixDelete` must still know where in the tree to start rebalancing. + var deleted_color = node.color; var replacement: ?*Node = null; + var replacement_parent: ?*Node = null; if (node.left == null) { replacement = node.right; + replacement_parent = node.parent; self.transplant(node, node.right); } else if (node.right == null) { replacement = node.left; + replacement_parent = node.parent; self.transplant(node, node.left); } else { - deleted_node = self.findMinimum(node.right.?); - deleted_color = deleted_node.color; - replacement = deleted_node.right; - - if (deleted_node.parent == node) { - if (replacement) |r| r.parent = deleted_node; + const successor = self.findMinimum(node.right.?); + deleted_color = successor.color; + replacement = successor.right; + + if (successor.parent == node) { + // The successor moves into `node`'s slot, so it becomes the + // parent of its own right child (the replacement). + replacement_parent = successor; } else { - self.transplant(deleted_node, deleted_node.right); - deleted_node.right = node.right; - if (deleted_node.right) |right| right.parent = deleted_node; + replacement_parent = successor.parent; + self.transplant(successor, successor.right); + successor.right = node.right; + if (successor.right) |right| right.parent = successor; } - self.transplant(node, deleted_node); - deleted_node.left = node.left; - if (deleted_node.left) |left| left.parent = deleted_node; - deleted_node.color = node.color; + self.transplant(node, successor); + successor.left = node.left; + if (successor.left) |left| left.parent = successor; + successor.color = node.color; } - // Fix red-black properties before freeing the node + // Fix red-black properties before freeing the node. if (deleted_color == .black) { - self.fixDelete(replacement); + self.fixDelete(replacement, replacement_parent); } self.allocator.destroy(node); } - fn fixDelete(self: *Self, node: ?*Node) void { + fn fixDelete(self: *Self, node: ?*Node, node_parent: ?*Node) void { var current = node; + // Tracked explicitly so the loop can advance even while `current` + // is null (a doubly-black NIL position). In a valid red-black tree + // a black, non-root node always has a sibling, so the sibling + // lookups below never dereference null. + var parent = node_parent; while (current != self.root and Node.isBlack(current)) { - if (current) |curr| { - const parent = curr.parent orelse break; + const p = parent.?; - if (curr == parent.left) { - var sibling = parent.right; + if (current == p.left) { + var sibling = p.right.?; - if (Node.isRed(sibling)) { - if (sibling) |s| s.color = .black; - parent.color = .red; - self.rotateLeft(parent); - sibling = parent.right; - } + if (sibling.color == .red) { + sibling.color = .black; + p.color = .red; + self.rotateLeft(p); + sibling = p.right.?; + } - if (sibling) |s| { - if (Node.isBlack(s.left) and Node.isBlack(s.right)) { - s.color = .red; - current = parent; - } else { - if (Node.isBlack(s.right)) { - if (s.left) |left| left.color = .black; - s.color = .red; - self.rotateRight(s); - sibling = parent.right; - } - - if (sibling) |new_s| { - new_s.color = parent.color; - parent.color = .black; - if (new_s.right) |right| right.color = .black; - self.rotateLeft(parent); - } - current = self.root; - } - } + if (Node.isBlack(sibling.left) and Node.isBlack(sibling.right)) { + sibling.color = .red; + current = p; + parent = p.parent; } else { - var sibling = parent.left; - - if (Node.isRed(sibling)) { - if (sibling) |s| s.color = .black; - parent.color = .red; - self.rotateRight(parent); - sibling = parent.left; + if (Node.isBlack(sibling.right)) { + if (sibling.left) |left| left.color = .black; + sibling.color = .red; + self.rotateRight(sibling); + sibling = p.right.?; } - if (sibling) |s| { - if (Node.isBlack(s.right) and Node.isBlack(s.left)) { - s.color = .red; - current = parent; - } else { - if (Node.isBlack(s.left)) { - if (s.right) |right| right.color = .black; - s.color = .red; - self.rotateLeft(s); - sibling = parent.left; - } - - if (sibling) |new_s| { - new_s.color = parent.color; - parent.color = .black; - if (new_s.left) |left| left.color = .black; - self.rotateRight(parent); - } - current = self.root; - } - } + sibling.color = p.color; + p.color = .black; + if (sibling.right) |right| right.color = .black; + self.rotateLeft(p); + current = self.root; } } else { - break; + var sibling = p.left.?; + + if (sibling.color == .red) { + sibling.color = .black; + p.color = .red; + self.rotateRight(p); + sibling = p.left.?; + } + + if (Node.isBlack(sibling.right) and Node.isBlack(sibling.left)) { + sibling.color = .red; + current = p; + parent = p.parent; + } else { + if (Node.isBlack(sibling.left)) { + if (sibling.right) |right| right.color = .black; + sibling.color = .red; + self.rotateLeft(sibling); + sibling = p.left.?; + } + + sibling.color = p.color; + p.color = .black; + if (sibling.left) |left| left.color = .black; + self.rotateRight(p); + current = self.root; + } } } @@ -548,6 +553,81 @@ fn i32Compare(lhs: i32, rhs: i32) std.math.Order { return std.math.order(lhs, rhs); } +const SetOracle = @import("oracle.zig").SetOracle; + +test "RedBlackTreeSet: differential test against sorted-array oracle" { + const allocator = std.testing.allocator; + var tree = RedBlackTreeSet(i32, i32Compare).init(allocator); + defer tree.deinit(); + + var oracle: SetOracle(i32, i32Compare) = .{}; + defer oracle.deinit(allocator); + + // Fixed seed keeps the operation sequence deterministic across runs. + var prng = std.Random.DefaultPrng.init(0x1234_5678_9abc_def0); + const random = prng.random(); + + const operations = 3000; + // A small key space forces frequent duplicate puts and removals of present + // values, exercising the rebalancing paths rather than only growth. + const key_space: u32 = 200; + + var op: usize = 0; + while (op < operations) : (op += 1) { + const value: i32 = @intCast(random.uintLessThan(u32, key_space)); + + // Roughly one removal for every two insertions, so the tree grows and + // then churns instead of only filling up. + if (random.uintLessThan(u32, 3) == 0) { + const tree_removed = tree.remove(value); + const oracle_removed = oracle.remove(value); + try std.testing.expectEqual(oracle_removed, tree_removed != null); + if (tree_removed) |v| try std.testing.expectEqual(value, v); + } else { + try tree.put(value); + try oracle.put(allocator, value); + } + + // Cheap invariants checked on every operation: the count must agree, the + // touched value's membership must agree, and in-order iteration must + // reproduce the oracle's sorted order exactly. + try std.testing.expectEqual(oracle.count(), tree.count()); + try std.testing.expectEqual(oracle.contains(value), tree.contains(value)); + try expectIterationMatches(&tree, &oracle); + + // The full key-space membership sweep is O(key_space) per check, so run + // it periodically rather than on every operation to keep the test fast. + if (op % 100 == 0) { + var k: i32 = 0; + while (k < @as(i32, @intCast(key_space))) : (k += 1) { + try std.testing.expectEqual(oracle.contains(k), tree.contains(k)); + } + } + } + + // Final full sweep after the last operation. + var k: i32 = 0; + while (k < @as(i32, @intCast(key_space))) : (k += 1) { + try std.testing.expectEqual(oracle.contains(k), tree.contains(k)); + } +} + +/// Asserts that a tree's in-order iteration reproduces the oracle's sorted +/// values exactly, in the same order and with the same length. +fn expectIterationMatches( + tree: *const RedBlackTreeSet(i32, i32Compare), + oracle: *const SetOracle(i32, i32Compare), +) !void { + var iter = try tree.iterator(); + defer iter.deinit(); + var idx: usize = 0; + while (try iter.next()) |item| : (idx += 1) { + try std.testing.expect(idx < oracle.items.items.len); + try std.testing.expectEqual(oracle.items.items[idx], item); + } + try std.testing.expectEqual(oracle.items.items.len, idx); +} + test "RedBlackTreeSet: basic operations" { const allocator = std.testing.allocator; var tree = RedBlackTreeSet(i32, i32Compare).init(allocator); @@ -744,3 +824,57 @@ test "RedBlackTreeSet: get returns correct value" { try std.testing.expect(tree.get(99) == null); } + +const RbTreeI32 = RedBlackTreeSet(i32, i32Compare); + +/// Returns the black height of a subtree while asserting the structural +/// red-black invariants: no red node has a red child, and every root-to-leaf +/// path through the subtree passes through the same number of black nodes. A +/// missing rebalance on deletion shows up here as unequal child black heights. +fn rbBlackHeight(node: ?*const RbTreeI32.Node) !usize { + const n = node orelse return 1; // NIL nodes are black. + if (n.color == .red) { + if (n.left) |l| try std.testing.expect(l.color == .black); + if (n.right) |r| try std.testing.expect(r.color == .black); + } + const left_height = try rbBlackHeight(n.left); + const right_height = try rbBlackHeight(n.right); + try std.testing.expectEqual(left_height, right_height); + return left_height + @as(usize, if (n.color == .black) 1 else 0); +} + +fn expectRbInvariants(tree: *const RbTreeI32) !void { + if (tree.root) |r| try std.testing.expect(r.color == .black); // The root is black. + _ = try rbBlackHeight(tree.root); +} + +test "regression: RedBlackTreeSet deletion preserves red-black invariants" { + // Bug found by the differential oracle: deleting a black node passed a null + // replacement to `fixDelete`, which (with no NIL sentinel) could not find its + // parent and skipped rebalancing. The tree stayed a valid BST, so ordering + // and count looked fine, but its black heights drifted out of balance and a + // later deletion drove `fixDelete` into an infinite loop. Checking the + // red-black invariants after each deletion catches the missing rebalance + // directly, at the first offending black-node removal. + const allocator = std.testing.allocator; + var tree = RbTreeI32.init(allocator); + defer tree.deinit(); + + const n: i32 = 64; + // Insert and delete in fixed permutations (multipliers coprime to 64), so the + // schedule is fully deterministic and reproducible. + var i: i32 = 0; + while (i < n) : (i += 1) try tree.put(@mod(i * 37, n)); + try std.testing.expectEqual(@as(usize, @intCast(n)), tree.count()); + try expectRbInvariants(&tree); + + i = 0; + while (i < n) : (i += 1) { + const key = @mod(i * 29, n); + try std.testing.expect(tree.remove(key) != null); + try std.testing.expect(!tree.contains(key)); + try std.testing.expectEqual(@as(usize, @intCast(n - 1 - i)), tree.count()); + try expectRbInvariants(&tree); + } + try std.testing.expectEqual(@as(usize, 0), tree.count()); +} diff --git a/src/ordered/skip_list_map.zig b/src/ordered/skip_list_map.zig index 4ea7e67..b13dd1c 100644 --- a/src/ordered/skip_list_map.zig +++ b/src/ordered/skip_list_map.zig @@ -300,6 +300,65 @@ fn i32Compare(lhs: i32, rhs: i32) std.math.Order { return std.math.order(lhs, rhs); } +const MapOracle = @import("oracle.zig").MapOracle; + +test "SkipListMap: differential test against sorted-array oracle" { + const allocator = std.testing.allocator; + var list = try SkipListMap(i32, i32, i32Compare, 16).init(allocator); + defer list.deinit(); + + var oracle: MapOracle(i32, i32, i32Compare) = .{}; + defer oracle.deinit(allocator); + + // The skip list seeds its own PRNG internally for level selection; this + // separate PRNG only drives the operation sequence and is fixed-seed so + // the sequence of keys and operations is deterministic. + var prng = std.Random.DefaultPrng.init(0x5151_5151_5151_5151); + const random = prng.random(); + + const operations = 3000; + const key_space: u32 = 200; + + var op: usize = 0; + while (op < operations) : (op += 1) { + const key: i32 = @intCast(random.uintLessThan(u32, key_space)); + const value: i32 = @intCast(op); + + if (random.uintLessThan(u32, 3) == 0) { + const removed = list.remove(key); + const oracle_removed = oracle.remove(key); + try std.testing.expectEqual(oracle_removed != null, removed != null); + if (oracle_removed) |ov| try std.testing.expectEqual(ov, removed.?); + } else { + try list.put(key, value); + try oracle.put(allocator, key, value); + } + + try std.testing.expectEqual(oracle.count(), list.count()); + if (oracle.get(key)) |ov| { + try std.testing.expectEqual(ov, list.get(key).?.*); + } else { + try std.testing.expect(list.get(key) == null); + } + + var iter = list.iterator(); + var idx: usize = 0; + while (iter.next()) |entry| : (idx += 1) { + try std.testing.expect(idx < oracle.entries.items.len); + try std.testing.expectEqual(oracle.entries.items[idx].key, entry.key); + try std.testing.expectEqual(oracle.entries.items[idx].value, entry.value); + } + try std.testing.expectEqual(oracle.entries.items.len, idx); + + if (op % 100 == 0) { + var k: i32 = 0; + while (k < @as(i32, @intCast(key_space))) : (k += 1) { + try std.testing.expectEqual(oracle.contains(k), list.contains(k)); + } + } + } +} + test "SkipListMap: basic operations" { const allocator = std.testing.allocator; var list = try SkipListMap(i32, []const u8, i32Compare, 16).init(allocator); diff --git a/src/ordered/sorted_set.zig b/src/ordered/sorted_set.zig index b7f6081..06918b9 100644 --- a/src/ordered/sorted_set.zig +++ b/src/ordered/sorted_set.zig @@ -110,6 +110,60 @@ fn i32Compare(lhs: i32, rhs: i32) std.math.Order { return std.math.order(lhs, rhs); } +const SetOracle = @import("oracle.zig").SetOracle; + +test "SortedSet: differential test against sorted-array oracle" { + const allocator = std.testing.allocator; + var set = SortedSet(i32, i32Compare).init(allocator); + defer set.deinit(); + + var oracle: SetOracle(i32, i32Compare) = .{}; + defer oracle.deinit(allocator); + + // Fixed seed keeps the operation sequence deterministic across runs. + var prng = std.Random.DefaultPrng.init(0x0bad_c0ffee_1234); + const random = prng.random(); + + const operations = 3000; + const key_space: u32 = 200; + + var op: usize = 0; + while (op < operations) : (op += 1) { + const value: i32 = @intCast(random.uintLessThan(u32, key_space)); + + if (random.uintLessThan(u32, 3) == 0) { + const set_removed = set.removeValue(value); + const oracle_removed = oracle.remove(value); + try std.testing.expectEqual(oracle_removed, set_removed != null); + if (set_removed) |v| try std.testing.expectEqual(value, v); + } else { + const set_added = try set.put(value); + const was_present = oracle.contains(value); + try oracle.put(allocator, value); + // `put` reports whether the value was newly added. + try std.testing.expectEqual(!was_present, set_added); + } + + try std.testing.expectEqual(oracle.count(), set.count()); + try std.testing.expectEqual(oracle.contains(value), set.contains(value)); + + var iter = set.iterator(); + var idx: usize = 0; + while (iter.next()) |item| : (idx += 1) { + try std.testing.expect(idx < oracle.items.items.len); + try std.testing.expectEqual(oracle.items.items[idx], item); + } + try std.testing.expectEqual(oracle.items.items.len, idx); + + if (op % 100 == 0) { + var k: i32 = 0; + while (k < @as(i32, @intCast(key_space))) : (k += 1) { + try std.testing.expectEqual(oracle.contains(k), set.contains(k)); + } + } + } +} + test "SortedSet basic functionality" { const allocator = std.testing.allocator; var set = SortedSet(i32, i32Compare).init(allocator); diff --git a/src/ordered/trie_map.zig b/src/ordered/trie_map.zig index 2375137..102200d 100644 --- a/src/ordered/trie_map.zig +++ b/src/ordered/trie_map.zig @@ -267,7 +267,7 @@ pub fn TrieMap(comptime V: type) type { errdefer stack.deinit(allocator); try stack.append(allocator, PrefixIteratorFrame{ .node = prefix_node.?, - .child_iter = prefix_node.?.children.iterator(), + .next_char = 0, .visited_self = false, }); @@ -286,7 +286,9 @@ pub fn TrieMap(comptime V: type) type { pub const PrefixIteratorFrame = struct { node: *const TrieNode, - child_iter: std.HashMap(u8, *TrieNode, std.hash_map.AutoContext(u8), std.hash_map.default_max_load_percentage).Iterator, + // Next child byte to examine, scanned over 0..256 so children are + // visited in ascending byte order, yielding keys in sorted order. + next_char: u16, visited_self: bool, }; @@ -303,22 +305,34 @@ pub fn TrieMap(comptime V: type) type { pub fn next(self: *PrefixIterator) !?[]const u8 { while (self.stack.items.len > 0) { - var frame = &self.stack.items[self.stack.items.len - 1]; + const top = self.stack.items.len - 1; - if (!frame.visited_self and frame.node.is_end) { - frame.visited_self = true; + if (!self.stack.items[top].visited_self and self.stack.items[top].node.is_end) { + self.stack.items[top].visited_self = true; return self.current_key.items; } - if (frame.child_iter.next()) |entry| { - const char = entry.key_ptr.*; - const child = entry.value_ptr.*; - - try self.current_key.append(self.allocator, char); + // Scan ascending byte values for the next existing child. + const node = self.stack.items[top].node; + var next_child: ?*TrieNode = null; + var next_byte: u8 = 0; + while (self.stack.items[top].next_char < 256) { + const char: u8 = @intCast(self.stack.items[top].next_char); + self.stack.items[top].next_char += 1; + if (node.children.get(char)) |child| { + next_child = child; + next_byte = char; + break; + } + } + if (next_child) |child| { + try self.current_key.append(self.allocator, next_byte); + // This append may reallocate `stack`, so `top` is + // recomputed on the next loop iteration rather than reused. try self.stack.append(self.allocator, PrefixIteratorFrame{ .node = child, - .child_iter = child.children.iterator(), + .next_char = 0, .visited_self = false, }); } else { @@ -340,22 +354,29 @@ pub fn TrieMap(comptime V: type) type { const IteratorFrame = struct { node: *const TrieNode, - child_iter: std.HashMap(u8, *TrieNode, std.hash_map.AutoContext(u8), std.hash_map.default_max_load_percentage).Iterator, + // Next child byte to examine, scanned over 0..256 so children + // are visited in ascending byte order. This makes the iterator + // yield keys in sorted (lexicographic) order, which a HashMap + // child iterator would not. + next_char: u16, visited_self: bool, }; fn init(allocator: std.mem.Allocator, root: *const TrieNode) !Iterator { - var stack: std.ArrayList(IteratorFrame) = .{}; + var stack: std.ArrayList(IteratorFrame) = .empty; + // The append below may fail with OOM; without this errdefer the + // stack's heap buffer would leak before the Iterator is returned. + errdefer stack.deinit(allocator); try stack.append(allocator, IteratorFrame{ .node = root, - .child_iter = root.children.iterator(), + .next_char = 0, .visited_self = false, }); return Iterator{ .stack = stack, .allocator = allocator, - .current_key = .{}, + .current_key = .empty, }; } @@ -366,22 +387,38 @@ pub fn TrieMap(comptime V: type) type { pub fn next(self: *Iterator) !?struct { key: []const u8, value: V } { while (self.stack.items.len > 0) { - var frame = &self.stack.items[self.stack.items.len - 1]; - - if (!frame.visited_self and frame.node.is_end) { - frame.visited_self = true; - return .{ .key = self.current_key.items, .value = frame.node.value.? }; + const top = self.stack.items.len - 1; + + if (!self.stack.items[top].visited_self and self.stack.items[top].node.is_end) { + self.stack.items[top].visited_self = true; + return .{ + .key = self.current_key.items, + .value = self.stack.items[top].node.value.?, + }; } - if (frame.child_iter.next()) |entry| { - const char = entry.key_ptr.*; - const child = entry.value_ptr.*; - - try self.current_key.append(self.allocator, char); + // Scan ascending byte values for the next existing child. + const node = self.stack.items[top].node; + var next_child: ?*TrieNode = null; + var next_byte: u8 = 0; + while (self.stack.items[top].next_char < 256) { + const char: u8 = @intCast(self.stack.items[top].next_char); + self.stack.items[top].next_char += 1; + if (node.children.get(char)) |child| { + next_child = child; + next_byte = char; + break; + } + } + if (next_child) |child| { + try self.current_key.append(self.allocator, next_byte); + // This append may reallocate `stack`, so the `top` index + // is recomputed on the next loop iteration rather than + // reused. try self.stack.append(self.allocator, IteratorFrame{ .node = child, - .child_iter = child.children.iterator(), + .next_char = 0, .visited_self = false, }); } else { @@ -401,6 +438,71 @@ pub fn TrieMap(comptime V: type) type { }; } +fn strCompare(lhs: []const u8, rhs: []const u8) std.math.Order { + return std.mem.order(u8, lhs, rhs); +} + +const MapOracle = @import("oracle.zig").MapOracle; + +test "TrieMap: differential test against sorted-array oracle" { + const allocator = std.testing.allocator; + + // Generated keys are duped into an arena that lives for the whole test, so + // the oracle can safely hold the same slices the trie was given. + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + const key_arena = arena.allocator(); + + var trie = try TrieMap(i32).init(allocator); + defer trie.deinit(); + + var oracle: MapOracle([]const u8, i32, strCompare) = .{}; + defer oracle.deinit(allocator); + + var prng = std.Random.DefaultPrng.init(0x7271_e000_0000_0000); + const random = prng.random(); + + const operations = 3000; + + var op: usize = 0; + while (op < operations) : (op += 1) { + // Short keys over a 4-letter alphabet force shared prefixes, exact + // duplicates, and prefix-is-also-a-key cases, exercising node pruning. + var buf: [4]u8 = undefined; + const len = random.uintLessThan(usize, buf.len + 1); + for (0..len) |i| buf[i] = 'a' + @as(u8, @intCast(random.uintLessThan(u8, 4))); + const key = try key_arena.dupe(u8, buf[0..len]); + const value: i32 = @intCast(op); + + if (random.uintLessThan(u32, 3) == 0) { + const removed = trie.remove(key); + const oracle_removed = oracle.remove(key); + try std.testing.expectEqual(oracle_removed != null, removed != null); + if (oracle_removed) |ov| try std.testing.expectEqual(ov, removed.?); + } else { + try trie.put(key, value); + try oracle.put(allocator, key, value); + } + + try std.testing.expectEqual(oracle.count(), trie.count()); + if (oracle.get(key)) |ov| { + try std.testing.expectEqual(ov, trie.get(key).?.*); + } else { + try std.testing.expect(trie.get(key) == null); + } + + var iter = try trie.iterator(); + defer iter.deinit(); + var idx: usize = 0; + while (try iter.next()) |entry| : (idx += 1) { + try std.testing.expect(idx < oracle.entries.items.len); + try std.testing.expectEqualStrings(oracle.entries.items[idx].key, entry.key); + try std.testing.expectEqual(oracle.entries.items[idx].value, entry.value); + } + try std.testing.expectEqual(oracle.entries.items.len, idx); + } +} + test "TrieMap: basic operations" { const allocator = std.testing.allocator; var trie = try TrieMap(i32).init(allocator); @@ -605,3 +707,25 @@ test "TrieMap: special characters" { try std.testing.expectEqual(@as(i32, 2), trie.get("test_case").?.*); try std.testing.expectEqual(@as(i32, 3), trie.get("foo.bar").?.*); } + +test "TrieMap: keysWithPrefix yields keys in sorted order" { + const allocator = std.testing.allocator; + var trie = try TrieMap(i32).init(allocator); + defer trie.deinit(); + + // Insert in an order that is neither sorted nor hash order. + const keys = [_][]const u8{ "bandit", "ban", "bandana", "band", "banana", "bee", "apex" }; + for (keys, 0..) |k, i| try trie.put(k, @intCast(i)); + + var iter = try trie.keysWithPrefix(allocator, "ban"); + defer iter.deinit(); + + // Keys under the "ban" prefix must come out in lexicographic order. + const expected = [_][]const u8{ "ban", "banana", "band", "bandana", "bandit" }; + var idx: usize = 0; + while (try iter.next()) |k| : (idx += 1) { + try std.testing.expect(idx < expected.len); + try std.testing.expectEqualStrings(expected[idx], k); + } + try std.testing.expectEqual(expected.len, idx); +} diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..585a860 --- /dev/null +++ b/uv.lock @@ -0,0 +1,199 @@ +version = 1 +revision = 2 +requires-python = ">=3.10, <4.0" + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/dc/be6cbe99670cd6e4ad387123647cb08e0c32975e223f82551e914c5568a6/filelock-3.29.4.tar.gz", hash = "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a", size = 63028, upload-time = "2026-06-13T16:12:00.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" }, +] + +[[package]] +name = "identify" +version = "2.6.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "ordered" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pre-commit" }, + { name = "python-dotenv" }, +] + +[package.metadata] +requires-dist = [ + { name = "pre-commit", specifier = ">=4.2.0,<5.0.0" }, + { name = "python-dotenv", specifier = ">=1.1.0,<2.0.0" }, +] + +[[package]] +name = "platformdirs" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/1a/cbbaf13b730abb0a16b964d984e19f2fe520c21a4dc664051359a3f5a9e7/python_discovery-1.4.2.tar.gz", hash = "sha256:8f3746c4b4968d22afbb97d36e1a0e5b66e6c0f297290f2e95f05b9b8bf18690", size = 70277, upload-time = "2026-06-11T16:10:42.383Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl", hash = "sha256:475803f53b7b2ed6e490e27373f9d8340f7d2eebf9acdaf645d7d714c97bb500", size = 33886, upload-time = "2026-06-11T16:10:41.192Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/a5/81f987504738e6defeed61ec1c47e2aefab3c35d8eeb87e1b3f38cf28254/virtualenv-21.5.1.tar.gz", hash = "sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8", size = 4578798, upload-time = "2026-06-16T16:23:58.603Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", size = 4558820, upload-time = "2026-06-16T16:23:56.963Z" }, +]