a collection of lightweight TypeScript packages for AT Protocol, the protocol powering Bluesky
atproto bluesky typescript npm

refactor(mst): flesh it out even more!

mary.my.id bebc93c4 e17e21a2

verified
Changed files
+527 -188
packages
+2
mise.toml
··· 2 2 bun = "1.2.13" 3 3 node = "22" 4 4 pnpm = "10" 5 + python = "3.11" 6 + uv = "latest"
+4
packages/utilities/mst/.gitignore
··· 1 + /mst-test-suite/.venv/ 2 + 3 + /mst-test-suite/cars/**/*.car 4 + /mst-test-suite/tests/**/*.json
-12
packages/utilities/mst/lib/errors.ts
··· 18 18 super(`missing block in store; cid=${cid}` + (def ? `; type=${def}` : ``)); 19 19 } 20 20 } 21 - 22 - /** 23 - * thrown when a block's decoded object doesn't match the expected type 24 - */ 25 - export class UnexpectedObjectError extends Error { 26 - constructor( 27 - public cid: string, 28 - public def: string, 29 - ) { 30 - super(`unexpected object in store; cid=${cid}; expected=${def}`); 31 - } 32 - }
+8 -8
packages/utilities/mst/lib/node-wrangler.ts
··· 4 4 import { NodeStore } from './node-store.js'; 5 5 6 6 /** 7 - * array helper: replaces element at index with a new value 7 + * replaces element at index with a new value 8 8 */ 9 9 const replaceAt = <T>(arr: readonly T[], index: number, value: T): readonly T[] => { 10 - return [...arr.slice(0, index), value, ...arr.slice(index + 1)]; 10 + return arr.with(index, value); 11 11 }; 12 12 13 13 /** 14 - * array helper: inserts element at index 14 + * inserts element at index 15 15 */ 16 16 const insertAt = <T>(arr: readonly T[], index: number, value: T): readonly T[] => { 17 - return [...arr.slice(0, index), value, ...arr.slice(index)]; 17 + return arr.toSpliced(index, 0, value); 18 18 }; 19 19 20 20 /** 21 - * array helper: removes element at index 21 + * removes element at index 22 22 */ 23 23 const removeAt = <T>(arr: readonly T[], index: number): readonly T[] => { 24 - return [...arr.slice(0, index), ...arr.slice(index + 1)]; 24 + return arr.toSpliced(index, 1); 25 25 }; 26 26 27 27 /** ··· 31 31 * the external APIs take a CID (the MST root) and return a CID (the new root), 32 32 * while storing any newly created nodes in the NodeStore. 33 33 * 34 - * neither method should ever fail - deleting a node that doesn't exist is a nop, 35 - * and adding the same node twice with the same value is also a nop. Callers 34 + * neither method should ever fail - deleting a node that doesn't exist is a noop, 35 + * and adding the same node twice with the same value is also a nop. callers 36 36 * can detect these cases by seeing if the initial and final CIDs changed. 37 37 */ 38 38 export class NodeWrangler {
+2
packages/utilities/mst/lib/node.ts
··· 147 147 t: subtrees[idx + 1], 148 148 v: values[idx], 149 149 }); 150 + 151 + prevKey = key; 150 152 } 151 153 152 154 const n: NodeData = {
+30 -45
packages/utilities/mst/lib/stores.ts
··· 1 - import * as CBOR from '@atcute/cbor'; 2 - 3 1 import { deleteMany, setMany, type BlockMap } from './blockmap.js'; 4 - import { MissingBlockError, UnexpectedObjectError } from './errors.js'; 5 2 6 3 /** 7 4 * a read-only interface for retrieving blocks by their CID ··· 132 129 * all writes go to the upper store only 133 130 */ 134 131 export class OverlayBlockStore implements BlockStore { 135 - /** the writable upper layer store */ 132 + /** writable upper layer store */ 136 133 upper: BlockStore; 137 - /** the read-only lower layer store */ 134 + /** read-only lower layer store */ 138 135 lower: ReadonlyBlockStore; 139 136 140 137 /** ··· 195 192 } 196 193 197 194 /** 198 - * reads and decodes a block, validating it matches the expected type 199 - * @param store block store to read from 200 - * @param cid CID of the block to read 201 - * @param def schema definition with name and validation function 202 - * @returns the decoded and validated object 203 - * @throws {MissingBlockError} if block is not found 204 - * @throws {UnexpectedObjectError} if block doesn't match expected type 195 + * a read-only block store wrapper that tracks all get() accesses 196 + * useful for collecting proof nodes during MST operations 205 197 */ 206 - export const readObject = async <T>(store: ReadonlyBlockStore, cid: string, def: CheckDef<T>): Promise<T> => { 207 - const bytes = await store.get(cid); 208 - if (bytes === null) { 209 - throw new MissingBlockError(cid, def.name); 210 - } 198 + export class LoggingBlockStore implements ReadonlyBlockStore { 199 + /** block store being proxied */ 200 + readonly wrapped: ReadonlyBlockStore; 201 + /** set of CIDs that were accessed via get() or getMany() */ 202 + readonly accessed = new Set<string>(); 211 203 212 - const decoded = CBOR.decode(bytes); 213 - if (!def.check(decoded)) { 214 - throw new UnexpectedObjectError(cid, def.name); 204 + /** 205 + * creates a new logging block store wrapper 206 + * @param store the block store to wrap 207 + */ 208 + constructor(store: ReadonlyBlockStore) { 209 + this.wrapped = store; 215 210 } 216 211 217 - return decoded; 218 - }; 212 + async get(cid: string): Promise<Uint8Array<ArrayBuffer> | null> { 213 + this.accessed.add(cid); 219 214 220 - /** 221 - * reads and decodes a block without type validation 222 - * @param store block store to read from 223 - * @param cid CID of the block to read 224 - * @returns the decoded object 225 - * @throws {MissingBlockError} if block is not found 226 - */ 227 - export const readRecord = async (store: ReadonlyBlockStore, cid: string): Promise<unknown> => { 228 - const bytes = await store.get(cid); 229 - if (bytes === null) { 230 - throw new MissingBlockError(cid, undefined); 215 + return this.wrapped.get(cid); 231 216 } 232 217 233 - const decoded = CBOR.decode(bytes); 218 + async getMany(cids: string[]): Promise<{ found: BlockMap; missing: string[] }> { 219 + const accessed = this.accessed; 234 220 235 - return decoded; 236 - }; 221 + for (const cid of cids) { 222 + accessed.add(cid); 223 + } 237 224 238 - /** 239 - * defines a type validator for use with readObject 240 - * combines a human-readable type name with a type guard function 241 - */ 242 - export interface CheckDef<T> { 243 - /** human-readable name of the expected type */ 244 - name: string; 245 - /** type guard function to validate the decoded value */ 246 - check: (value: unknown) => value is T; 225 + return this.wrapped.getMany(cids); 226 + } 227 + 228 + async has(cid: string): Promise<boolean> { 229 + // has() doesn't count as an access for proof purposes 230 + return this.wrapped.has(cid); 231 + } 247 232 }
+157 -114
packages/utilities/mst/lib/test-suite.test.ts
··· 1 - import { readFileSync, readdirSync, statSync } from 'node:fs'; 2 - import { join } from 'node:path'; 3 - import { describe, expect, it } from 'vitest'; 1 + import { beforeAll, describe, expect, it } from 'vitest'; 2 + import * as v from 'valibot'; 3 + 4 + import * as fs from 'node:fs/promises'; 5 + import * as path from 'node:path'; 4 6 5 7 import { fromUint8Array } from '@atcute/car/v4/car-reader'; 6 8 import * as CID from '@atcute/cid'; 7 9 10 + import { setMany } from './blockmap.js'; 8 11 import { DeltaType, mstDiff, recordDiff } from './diff.js'; 9 12 import { NodeStore } from './node-store.js'; 10 - import { MemoryBlockStore } from './stores.js'; 13 + import { NodeWrangler } from './node-wrangler.js'; 14 + import { buildExclusionProof, buildInclusionProof } from './proof.js'; 15 + import { 16 + LoggingBlockStore, 17 + MemoryBlockStore, 18 + OverlayBlockStore, 19 + ReadonlyMemoryBlockStore, 20 + } from './stores.js'; 11 21 12 - interface MstDiffTestCase { 13 - $type: 'mst-diff'; 14 - description: string; 15 - inputs: { 16 - mst_a: string; 17 - mst_b: string; 18 - }; 19 - results: { 20 - created_nodes: string[]; 21 - deleted_nodes: string[]; 22 - record_ops: Array<{ 23 - rpath: string; 24 - old_value: string | null; 25 - new_value: string | null; 26 - }>; 27 - proof_nodes: string[]; 28 - inductive_proof_nodes: string[]; 29 - firehose_cids: string | string[]; 30 - }; 31 - } 22 + const mstDiffTestCaseSchema = v.object({ 23 + $type: v.literal('mst-diff'), 24 + description: v.string(), 25 + inputs: v.object({ 26 + mst_a: v.string(), 27 + mst_b: v.string(), 28 + }), 29 + results: v.object({ 30 + created_nodes: v.array(v.string()), 31 + deleted_nodes: v.array(v.string()), 32 + record_ops: v.array( 33 + v.object({ 34 + rpath: v.string(), 35 + old_value: v.nullable(v.string()), 36 + new_value: v.nullable(v.string()), 37 + }), 38 + ), 39 + proof_nodes: v.array(v.string()), 40 + inductive_proof_nodes: v.array(v.string()), 41 + }), 42 + }); 43 + 44 + type MstDiffTestCase = v.InferOutput<typeof mstDiffTestCaseSchema>; 45 + 46 + const testSuiteRoot = path.join(__dirname, '../mst-test-suite'); 32 47 33 48 /** 34 49 * Load a CAR file into a MemoryBlockStore and extract the root CID 35 50 */ 36 - const loadCar = (carPath: string): { store: MemoryBlockStore; root: string } => { 37 - const testSuiteRoot = join(__dirname, '..', '.research', 'mst-test-suite'); 38 - const fullPath = join(testSuiteRoot, carPath); 39 - const carBytes = readFileSync(fullPath); 51 + const loadCar = async (relname: string): Promise<{ store: ReadonlyMemoryBlockStore; root: string }> => { 52 + const filename = path.join(testSuiteRoot, relname); 53 + const bytes = await fs.readFile(filename); 40 54 41 - const car = fromUint8Array(carBytes); 55 + const car = fromUint8Array(bytes); 42 56 const store = new MemoryBlockStore(); 43 57 44 - // Load all blocks from CAR into the store 45 58 for (const entry of car) { 46 59 const cidStr = CID.toCidLink(entry.cid).$link; 47 - store.blocks.set(cidStr, entry.bytes); 60 + store.blocks.set(cidStr, entry.bytes as Uint8Array<ArrayBuffer>); 48 61 } 49 62 50 - // Extract root CID from CAR header 51 63 if (car.roots.length !== 1) { 52 - throw new Error(`Expected exactly 1 root in CAR, got ${car.roots.length}`); 64 + throw new Error(`expected exactly 1 root in CAR, got ${car.roots.length}`); 53 65 } 54 66 55 67 const root = car.roots[0].$link; 56 68 return { store, root }; 57 69 }; 58 70 59 - /** 60 - * Recursively find all .json test files in a directory 61 - */ 62 - const findTestFiles = (dir: string): string[] => { 63 - const results: string[] = []; 64 - const entries = readdirSync(dir); 65 - 66 - for (const entry of entries) { 67 - const fullPath = join(dir, entry); 68 - const stat = statSync(fullPath); 71 + const testCases = await (async () => { 72 + const testsDir = path.join(testSuiteRoot, 'tests'); 69 73 70 - if (stat.isDirectory()) { 71 - results.push(...findTestFiles(fullPath)); 72 - } else if (entry.endsWith('.json')) { 73 - results.push(fullPath); 74 - } 75 - } 74 + const testCases: Array<{ path: string; description: string; testCase: MstDiffTestCase }> = []; 76 75 77 - return results; 78 - }; 79 - 80 - /** 81 - * Load all test cases from the test suite 82 - */ 83 - const loadTestCases = (): Array<{ path: string; testCase: MstDiffTestCase }> => { 84 - const testSuiteRoot = join(__dirname, '..', '.research', 'mst-test-suite'); 85 - const testsDir = join(testSuiteRoot, 'tests'); 86 - const testFiles = findTestFiles(testsDir); 76 + for await (const name of fs.glob('**/*.json', { cwd: testsDir })) { 77 + const filename = path.join(testsDir, name); 87 78 88 - const testCases: Array<{ path: string; testCase: MstDiffTestCase }> = []; 79 + const raw = await fs.readFile(filename, 'utf-8'); 80 + const json = JSON.parse(raw); 89 81 90 - for (const filePath of testFiles) { 91 - const content = readFileSync(filePath, 'utf-8'); 92 - const testCase = JSON.parse(content) as MstDiffTestCase; 82 + const testCase = v.parse(mstDiffTestCaseSchema, json); 93 83 94 - if (testCase.$type === 'mst-diff') { 95 - testCases.push({ path: filePath, testCase }); 96 - } 84 + testCases.push({ 85 + path: filename, 86 + description: testCase.description.replace(`procedurally generated MST diff test case `, ``), 87 + testCase, 88 + }); 97 89 } 98 90 99 91 return testCases; 100 - }; 92 + })(); 101 93 102 94 describe('MST Test Suite', () => { 103 - const allTestCases = loadTestCases(); 95 + describe.each(testCases)('$description', ({ testCase }) => { 96 + let storeA: ReadonlyMemoryBlockStore; 97 + let rootA: string; 104 98 105 - // Run all test cases 106 - const testCases = allTestCases; 99 + let storeB: ReadonlyMemoryBlockStore; 100 + let rootB: string; 107 101 108 - it(`should have loaded test cases (${testCases.length} total)`, () => { 109 - expect(testCases.length).toBeGreaterThan(1000); // Should have 16k+ tests 110 - }); 102 + beforeAll(async () => { 103 + ({ store: storeA, root: rootA } = await loadCar(testCase.inputs.mst_a)); 104 + ({ store: storeB, root: rootB } = await loadCar(testCase.inputs.mst_b)); 105 + }); 111 106 112 - describe.each(testCases)('$testCase.description', ({ testCase }) => { 113 - it('should compute correct mstDiff', async () => { 114 - // Load both CARs 115 - const { store: storeA, root: rootA } = loadCar(testCase.inputs.mst_a); 116 - const { store: storeB, root: rootB } = loadCar(testCase.inputs.mst_b); 117 - 118 - // Create NodeStores (combine both block stores for access to all blocks) 119 - // We need an overlay approach since diff needs to read from both trees 107 + it('computes the correct mstDiff', async () => { 120 108 const combinedStore = new MemoryBlockStore(); 121 - for (const [cid, bytes] of storeA.blocks) { 122 - combinedStore.blocks.set(cid, bytes); 123 - } 124 - for (const [cid, bytes] of storeB.blocks) { 125 - combinedStore.blocks.set(cid, bytes); 126 - } 109 + setMany(combinedStore.blocks, storeA.blocks); 110 + setMany(combinedStore.blocks, storeB.blocks); 127 111 128 112 const nodeStore = new NodeStore(combinedStore); 129 113 130 - // Run mstDiff 131 114 const [createdNodes, deletedNodes] = await mstDiff(nodeStore, rootA, rootB); 132 115 133 - // Compare created_nodes (as sets, order doesn't matter) 134 116 const expectedCreated = new Set(testCase.results.created_nodes); 135 117 expect(createdNodes).toEqual(expectedCreated); 136 118 137 - // Compare deleted_nodes (as sets, order doesn't matter) 138 119 const expectedDeleted = new Set(testCase.results.deleted_nodes); 139 120 expect(deletedNodes).toEqual(expectedDeleted); 140 121 }); 141 122 142 - it('should compute correct recordDiff', async () => { 143 - // Load both CARs 144 - const { store: storeA, root: rootA } = loadCar(testCase.inputs.mst_a); 145 - const { store: storeB, root: rootB } = loadCar(testCase.inputs.mst_b); 146 - 147 - // Create combined NodeStore 123 + it('computes the correct recordDiff', async () => { 148 124 const combinedStore = new MemoryBlockStore(); 149 - for (const [cid, bytes] of storeA.blocks) { 150 - combinedStore.blocks.set(cid, bytes); 151 - } 152 - for (const [cid, bytes] of storeB.blocks) { 153 - combinedStore.blocks.set(cid, bytes); 154 - } 125 + setMany(combinedStore.blocks, storeA.blocks); 126 + setMany(combinedStore.blocks, storeB.blocks); 155 127 156 128 const nodeStore = new NodeStore(combinedStore); 157 129 158 - // Run mstDiff and recordDiff 159 130 const [createdNodes, deletedNodes] = await mstDiff(nodeStore, rootA, rootB); 160 131 161 - const deltas = []; 162 - for await (const delta of recordDiff(nodeStore, createdNodes, deletedNodes)) { 163 - deltas.push(delta); 164 - } 132 + const deltas = await Array.fromAsync(recordDiff(nodeStore, createdNodes, deletedNodes)); 133 + deltas.sort((a, b) => +(a.path > b.path) - +(a.path < b.path)); 165 134 166 - // Sort both actual and expected by rpath for comparison 167 - const sortedDeltas = deltas.sort((a, b) => a.path.localeCompare(b.path)); 168 - const sortedExpected = [...testCase.results.record_ops].sort((a, b) => a.rpath.localeCompare(b.rpath)); 135 + const expectance = testCase.results.record_ops.toSorted( 136 + (a, b) => +(a.rpath > b.rpath) - +(a.rpath < b.rpath), 137 + ); 169 138 170 - expect(sortedDeltas.length).toBe(sortedExpected.length); 139 + expect(deltas.length).toBe(expectance.length); 171 140 172 - for (let i = 0; i < sortedDeltas.length; i++) { 173 - const actual = sortedDeltas[i]; 174 - const expected = sortedExpected[i]; 141 + for (let idx = 0, len = deltas.length; idx < len; idx++) { 142 + const actual = deltas[idx]; 143 + const expected = expectance[idx]; 175 144 176 145 expect(actual.path).toBe(expected.rpath); 177 146 expect(actual.priorValue?.$link ?? null).toBe(expected.old_value); ··· 186 155 expect(actual.deltaType).toBe(DeltaType.UPDATED); 187 156 } 188 157 } 158 + }); 159 + 160 + it('computes the correct proof_nodes', async () => { 161 + // create combined store 162 + const combinedStore = new MemoryBlockStore(); 163 + setMany(combinedStore.blocks, storeA.blocks); 164 + setMany(combinedStore.blocks, storeB.blocks); 165 + 166 + const nodeStore = new NodeStore(combinedStore); 167 + 168 + // collect proof nodes for all record operations 169 + const proofNodes = new Set<string>(); 170 + 171 + for (const op of testCase.results.record_ops) { 172 + let proof: Set<string>; 173 + 174 + if (op.old_value === null) { 175 + // CREATED: inclusion proof for new record in rootB 176 + proof = await buildInclusionProof(nodeStore, rootB, op.rpath); 177 + } else if (op.new_value === null) { 178 + // DELETED: exclusion proof in rootB 179 + proof = await buildExclusionProof(nodeStore, rootB, op.rpath); 180 + } else { 181 + // UPDATED: inclusion proof for updated record in rootB 182 + proof = await buildInclusionProof(nodeStore, rootB, op.rpath); 183 + } 184 + 185 + // add all proof nodes to the set 186 + for (const cid of proof) { 187 + proofNodes.add(cid); 188 + } 189 + } 190 + 191 + // compare against expected proof_nodes (as sets, order doesn't matter) 192 + const expectedProofNodes = new Set(testCase.results.proof_nodes); 193 + expect(proofNodes).toEqual(expectedProofNodes); 194 + }); 195 + 196 + it('computes the correct inductive_proof_nodes', async () => { 197 + // create combined store 198 + const combinedStore = new MemoryBlockStore(); 199 + setMany(combinedStore.blocks, storeA.blocks); 200 + setMany(combinedStore.blocks, storeB.blocks); 201 + 202 + // inductive proofs: nodes that get READ when applying ops in REVERSE order 203 + // this is used for MST operation inversion (verifying B→A instead of A→B) 204 + 205 + const loggingStore = new LoggingBlockStore(combinedStore); 206 + 207 + const overlayStore = new OverlayBlockStore(new MemoryBlockStore(), loggingStore); 208 + const nodeStore = new NodeStore(overlayStore); 209 + const wrangler = new NodeWrangler(nodeStore); 210 + 211 + // start from rootB and apply operations in REVERSE order 212 + let currentRoot = rootB; 213 + const reversedOps = testCase.results.record_ops.toReversed(); 214 + 215 + for (const op of reversedOps) { 216 + if (op.old_value === null) { 217 + // was CREATE, reverse it with DELETE 218 + currentRoot = await wrangler.deleteRecord(currentRoot, op.rpath); 219 + } else { 220 + // was UPDATE or DELETE, reverse with PUT of old value 221 + currentRoot = await wrangler.putRecord(currentRoot, op.rpath, { $link: op.old_value }); 222 + } 223 + } 224 + 225 + // after reversing all operations, we should end up back at rootA 226 + expect(currentRoot).toBe(rootA); 227 + 228 + // the blocks that were accessed (read) are the inductive proof nodes 229 + const inductiveProofNodes = loggingStore.accessed; 230 + const expectedInductiveProofNodes = new Set(testCase.results.inductive_proof_nodes); 231 + expect(inductiveProofNodes).toEqual(expectedInductiveProofNodes); 189 232 }); 190 233 }); 191 234 });
packages/utilities/mst/mst-test-suite/cars/exhaustive/.gitkeep

This is a binary file and will not be displayed.

+7
packages/utilities/mst/mst-test-suite/pyproject.toml
··· 1 + [project] 2 + name = "mst-test-suite" 3 + version = "0.1.0" 4 + dependencies = [ 5 + "atmst>=0.0.6", 6 + "cbrrr>=1.0.1", 7 + ]
+228
packages/utilities/mst/mst-test-suite/scripts/generate_exhaustive_cars.py
··· 1 + from typing import BinaryIO, Optional 2 + import json 3 + 4 + from atmst.mst.node import MSTNode 5 + from atmst.mst.node_store import NodeStore 6 + from atmst.mst.node_wrangler import NodeWrangler 7 + from atmst.mst.node_walker import NodeWalker 8 + from atmst.mst.diff import very_slow_mst_diff, record_diff 9 + from atmst.blockstore import MemoryBlockStore, OverlayBlockStore, BlockStore 10 + from atmst.blockstore.car_file import encode_varint 11 + from atmst.mst import proof 12 + import cbrrr 13 + from cbrrr import CID 14 + 15 + 16 + class LoggingBlockStoreWrapper(BlockStore): 17 + def __init__(self, bs: BlockStore): 18 + self.bs = bs 19 + self.gets = set() 20 + 21 + def put_block(self, key: bytes, value: bytes) -> None: 22 + self.bs.put_block(key, value) 23 + 24 + def get_block(self, key: bytes) -> bytes: 25 + self.gets.add(key) 26 + return self.bs.get_block(key) 27 + 28 + def del_block(self, key: bytes) -> None: 29 + self.bs.del_block(key) 30 + 31 + 32 + """ 33 + class LoggingNodeStore(NodeStore): 34 + def __init__(self, bs): 35 + self.read_cids = set() 36 + self.stored_cids = set() 37 + super().__init__(bs) 38 + 39 + def get_node(self, cid: Optional[CID]) -> MSTNode: 40 + if cid is None: 41 + self.read_cids.add(MSTNode.empty_root().cid) 42 + else: 43 + self.read_cids.add(cid) 44 + return super().get_node(cid) 45 + 46 + def stored_node(self, node: MSTNode) -> MSTNode: 47 + self.stored_cids.add(node.cid) 48 + return super().stored_node(node) 49 + """ 50 + 51 + 52 + class CarWriter: 53 + def __init__(self, stream: BinaryIO, root: cbrrr.CID) -> None: 54 + self.stream = stream 55 + header_bytes = cbrrr.encode_dag_cbor({"version": 1, "roots": [root]}) 56 + stream.write(encode_varint(len(header_bytes))) 57 + stream.write(header_bytes) 58 + 59 + def write_block(self, cid: cbrrr.CID, value: bytes): 60 + cid_bytes = bytes(cid) 61 + self.stream.write(encode_varint(len(cid_bytes) + len(value))) 62 + self.stream.write(cid_bytes) 63 + self.stream.write(value) 64 + 65 + 66 + keys = [] 67 + key_heights = [ 68 + 0, 69 + 1, 70 + 0, 71 + 2, 72 + 0, 73 + 1, 74 + 0, 75 + ] # if all these keys are added to a MST, it'll form a perfect binary tree. 76 + i = 0 77 + for height in key_heights: 78 + while True: 79 + key = f"k/{i:02d}" 80 + i += 1 81 + if MSTNode.key_height(key) == height: 82 + keys.append(key) 83 + break 84 + 85 + vals = [ 86 + CID.cidv1_dag_cbor_sha256_32_from( 87 + cbrrr.encode_dag_cbor({"$type": "mst-test-data", "value_for": k}) 88 + ) 89 + for k in keys 90 + ] 91 + 92 + val_for_key = dict(zip(keys, vals)) 93 + 94 + print(keys) 95 + print(vals) 96 + 97 + # we can reuse these 98 + bs = MemoryBlockStore() 99 + ns = NodeStore(bs) 100 + wrangler = NodeWrangler(ns) 101 + 102 + roots = [] 103 + 104 + for i in range(2 ** len(keys)): 105 + filename = f"./cars/exhaustive/exhaustive_{i:03d}.car" 106 + root = ns.get_node(None).cid 107 + for j in range(len(keys)): 108 + if (i >> j) & 1: 109 + # filename += f"_{keys[j]}h{key_heights[j]}" 110 + root = wrangler.put_record(root, keys[j], vals[j]) 111 + # filename += ".car" 112 + print(i, filename) 113 + 114 + car_blocks = [] 115 + for node in NodeWalker(ns, root).iter_nodes(): 116 + car_blocks.append((node.cid, node.serialised)) 117 + 118 + assert len(set(cid for cid, val in car_blocks)) == len(car_blocks) # no dupes 119 + 120 + with open(filename, "wb") as carfile: 121 + car = CarWriter(carfile, root) 122 + for cid, val in sorted(car_blocks, key=lambda x: bytes(x[0])): 123 + car.write_block(cid, val) 124 + 125 + roots.append(root) 126 + 127 + # collecting these stats just for the sake of curiosity 128 + # identical_proof_and_creation_count = 0 129 + # proof_superset_of_creation_count = 0 130 + # creation_superset_of_proof_count = 0 131 + inversion_needs_extra_blocks = 0 132 + clusion_proof_nodes_not_in_inversion_proof = 0 133 + 134 + # generate exhaustive test cases 135 + for ai, root_a in enumerate(roots): 136 + for bi, root_b in enumerate(roots): 137 + filename = f"./tests/diff/exhaustive/exhaustive_{ai:03d}_{bi:03d}.json" 138 + print(filename) 139 + car_a = f"./cars/exhaustive/exhaustive_{ai:03d}.car" 140 + car_b = f"./cars/exhaustive/exhaustive_{bi:03d}.car" 141 + created_nodes, deleted_nodes = very_slow_mst_diff(ns, root_a, root_b) 142 + record_ops = [] 143 + proof_nodes = set() 144 + no_deletions = True 145 + for delta in record_diff(ns, created_nodes, deleted_nodes): 146 + record_ops.append( 147 + { 148 + "rpath": delta.path, 149 + "old_value": None 150 + if delta.prior_value is None 151 + else delta.prior_value.encode(), 152 + "new_value": None 153 + if delta.later_value is None 154 + else delta.later_value.encode(), 155 + } 156 + ) 157 + if delta.later_value is None: # deletion 158 + proof_nodes.update(proof.build_exclusion_proof(ns, root_b, delta.path)) 159 + no_deletions = False 160 + else: # update or create 161 + proof_nodes.update(proof.build_inclusion_proof(ns, root_b, delta.path)) 162 + 163 + if no_deletions: # commits with no deletions are more well-behaved 164 + assert proof_nodes.issubset(created_nodes) 165 + 166 + # my inductive-proof-generation logic is ops order sensitive, so we do the sort beforehand 167 + # TODO: maybe "deletes first" or similar produces smaller proofs on average? 168 + record_ops.sort(key=lambda x: x["rpath"]) 169 + 170 + # figure out which blocks are required for inductive proofs. 171 + # the idea here is that we use an overlay blockstore and log every "get" that has to fall thru to the lower layer. 172 + # those gets are therefore the blocks required for a stateless consumer to verify the proof. 173 + upper = MemoryBlockStore() 174 + lbs = LoggingBlockStoreWrapper(bs) 175 + lns = NodeStore(OverlayBlockStore(upper, lbs)) 176 + lnw = NodeWrangler(lns) 177 + proof_root = root_b 178 + for op in record_ops[ 179 + ::-1 180 + ]: # while the order does not effect the final root CID, it does affect the set of CIDs that fall thru 181 + if op["old_value"] is None: 182 + proof_root = lnw.del_record(proof_root, op["rpath"]) 183 + else: 184 + proof_root = lnw.put_record( 185 + proof_root, op["rpath"], val_for_key[op["rpath"]] 186 + ) 187 + assert proof_root == root_a # we're back to where we started 188 + inductive_proof_nodes = set(CID(cid) for cid in lbs.gets) 189 + 190 + if inductive_proof_nodes - (created_nodes | proof_nodes): 191 + # print(delta) 192 + inversion_needs_extra_blocks += 1 193 + 194 + if proof_nodes - inductive_proof_nodes: 195 + clusion_proof_nodes_not_in_inversion_proof += 1 196 + 197 + # if proof_nodes == created_nodes: 198 + # identical_proof_and_creation_count += 1 199 + # if proof_nodes.issuperset(created_nodes): 200 + # proof_superset_of_creation_count += 1 201 + # if created_nodes.issuperset(proof_nodes): 202 + # creation_superset_of_proof_count += 1 203 + 204 + testcase = { 205 + "$type": "mst-diff", 206 + "description": f"procedurally generated MST diff test case between MST {ai} and {bi}", 207 + "inputs": {"mst_a": car_a, "mst_b": car_b}, 208 + "results": { 209 + "created_nodes": sorted([cid.encode() for cid in created_nodes]), 210 + "deleted_nodes": sorted([cid.encode() for cid in deleted_nodes]), 211 + "record_ops": record_ops, # these were sorted earlier 212 + "proof_nodes": sorted([cid.encode() for cid in proof_nodes]), 213 + "inductive_proof_nodes": sorted( 214 + [cid.encode() for cid in inductive_proof_nodes] 215 + ), 216 + "firehose_cids": "TODO", 217 + }, 218 + } 219 + with open(filename, "w") as jsonfile: 220 + json.dump(testcase, jsonfile, indent="\t") 221 + 222 + # print("identical_proof_and_creation_count", identical_proof_and_creation_count / (len(roots)**2)) # 0.75 223 + # print("proof_superset_of_creation_count", proof_superset_of_creation_count / (len(roots)**2)) # 0.84 224 + # print("creation_superset_of_proof_count", creation_superset_of_proof_count / (len(roots)**2)) # 0.91 225 + print( 226 + "inversion_needs_extra_blocks", inversion_needs_extra_blocks / (len(roots) ** 2) 227 + ) # 0.04 228 + print(clusion_proof_nodes_not_in_inversion_proof)
packages/utilities/mst/mst-test-suite/tests/diff/exhaustive/.gitkeep

This is a binary file and will not be displayed.

+81
packages/utilities/mst/mst-test-suite/uv.lock
··· 1 + version = 1 2 + revision = 3 3 + requires-python = ">=3.11" 4 + 5 + [[package]] 6 + name = "atmst" 7 + version = "0.0.6" 8 + source = { registry = "https://pypi.org/simple" } 9 + dependencies = [ 10 + { name = "cbrrr" }, 11 + { name = "lru-dict" }, 12 + { name = "more-itertools" }, 13 + ] 14 + sdist = { url = "https://files.pythonhosted.org/packages/47/7a/2cca04368b664d372473504615e37150466ecc796dff018504d8daf5de6d/atmst-0.0.6.tar.gz", hash = "sha256:bdc3ada3f234e28dada73f50cd40359534f99208436e831fe035f6fc7c7b188e", size = 18577, upload-time = "2024-12-21T11:37:20.15Z" } 15 + wheels = [ 16 + { url = "https://files.pythonhosted.org/packages/f1/ec/d743d809cadfaae230ebed08c00f63a68f1bfe82042f74ea98c51965ed8f/atmst-0.0.6-py3-none-any.whl", hash = "sha256:e63801225f31b602a3aacfe73360561fa5f488adb1a56d64e0746d462981ecff", size = 19744, upload-time = "2024-12-21T11:37:17.804Z" }, 17 + ] 18 + 19 + [[package]] 20 + name = "cbrrr" 21 + version = "1.0.1" 22 + source = { registry = "https://pypi.org/simple" } 23 + sdist = { url = "https://files.pythonhosted.org/packages/a3/2e/321b68b2b12c99864f0872feb8ce80f03483b59c113730f7cb4465a49e0e/cbrrr-1.0.1.tar.gz", hash = "sha256:2dc5f78a71b67849e1b364053819053f03cd2032797ce6adbe1d02e8d602698c", size = 17503, upload-time = "2024-11-16T22:59:41.164Z" } 24 + 25 + [[package]] 26 + name = "lru-dict" 27 + version = "1.3.0" 28 + source = { registry = "https://pypi.org/simple" } 29 + sdist = { url = "https://files.pythonhosted.org/packages/96/e3/42c87871920602a3c8300915bd0292f76eccc66c38f782397acbf8a62088/lru-dict-1.3.0.tar.gz", hash = "sha256:54fd1966d6bd1fcde781596cb86068214edeebff1db13a2cea11079e3fd07b6b", size = 13123, upload-time = "2023-11-06T01:40:12.951Z" } 30 + wheels = [ 31 + { url = "https://files.pythonhosted.org/packages/a8/c9/6fac0cb67160f0efa3cc76a6a7d04d5e21a516eeb991ebba08f4f8f01ec5/lru_dict-1.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:20c595764695d20bdc3ab9b582e0cc99814da183544afb83783a36d6741a0dac", size = 17750, upload-time = "2023-11-06T01:38:52.667Z" }, 32 + { url = "https://files.pythonhosted.org/packages/61/14/f90dee4bc547ae266dbeffd4e11611234bb6af511dea48f3bc8dac1de478/lru_dict-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d9b30a8f50c3fa72a494eca6be5810a1b5c89e4f0fda89374f0d1c5ad8d37d51", size = 11055, upload-time = "2023-11-06T01:38:53.798Z" }, 33 + { url = "https://files.pythonhosted.org/packages/4e/63/a0ae20525f9d52f62ac0def47935f8a2b3b6fcd2c145218b9a27fc1fb910/lru_dict-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9710737584650a4251b9a566cbb1a86f83437adb209c9ba43a4e756d12faf0d7", size = 11330, upload-time = "2023-11-06T01:38:54.847Z" }, 34 + { url = "https://files.pythonhosted.org/packages/e9/c6/8c2b81b61e5206910c81b712500736227289aefe4ccfb36137aa21807003/lru_dict-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b84c321ae34f2f40aae80e18b6fa08b31c90095792ab64bb99d2e385143effaa", size = 31793, upload-time = "2023-11-06T01:38:56.163Z" }, 35 + { url = "https://files.pythonhosted.org/packages/f9/d7/af9733f94df67a2e9e31ef47d4c41aff1836024f135cdbda4743eb628452/lru_dict-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eed24272b4121b7c22f234daed99899817d81d671b3ed030c876ac88bc9dc890", size = 33090, upload-time = "2023-11-06T01:38:57.091Z" }, 36 + { url = "https://files.pythonhosted.org/packages/5b/6e/5b09b069a70028bcf05dbdc57a301fbe8b3bafecf916f2ed5a3065c79a71/lru_dict-1.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd13af06dab7c6ee92284fd02ed9a5613a07d5c1b41948dc8886e7207f86dfd", size = 29795, upload-time = "2023-11-06T01:38:58.278Z" }, 37 + { url = "https://files.pythonhosted.org/packages/21/92/4690daefc2602f7c3429ecf54572d37a9e3c372d370344d2185daa4d5ecc/lru_dict-1.3.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1efc59bfba6aac33684d87b9e02813b0e2445b2f1c444dae2a0b396ad0ed60c", size = 31586, upload-time = "2023-11-06T01:38:59.363Z" }, 38 + { url = "https://files.pythonhosted.org/packages/3c/67/0a29a91087196b02f278d8765120ee4e7486f1f72a4c505fd1cd3109e627/lru_dict-1.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cfaf75ac574447afcf8ad998789071af11d2bcf6f947643231f692948839bd98", size = 36662, upload-time = "2023-11-06T01:39:00.795Z" }, 39 + { url = "https://files.pythonhosted.org/packages/36/54/8d56c514cd2333b652bd44c8f1962ab986cbe68e8ad7258c9e0f360cddb6/lru_dict-1.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c95f8751e2abd6f778da0399c8e0239321d560dbc58cb063827123137d213242", size = 35118, upload-time = "2023-11-06T01:39:01.883Z" }, 40 + { url = "https://files.pythonhosted.org/packages/f5/9a/c7a175d10d503b86974cb07141ca175947145dd1c7370fcda86fbbcaf326/lru_dict-1.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:abd0c284b26b5c4ee806ca4f33ab5e16b4bf4d5ec9e093e75a6f6287acdde78e", size = 38198, upload-time = "2023-11-06T01:39:03.306Z" }, 41 + { url = "https://files.pythonhosted.org/packages/fd/59/2e5086c8e8a05a7282a824a2a37e3c45cd5714e7b83d8bc0267cb3bb5b4f/lru_dict-1.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a47740652b25900ac5ce52667b2eade28d8b5fdca0ccd3323459df710e8210a", size = 36542, upload-time = "2023-11-06T01:39:04.751Z" }, 42 + { url = "https://files.pythonhosted.org/packages/12/52/80d0a06e5f45fe7c278dd662da6ea5b39f2ff003248f448189932f6b71c2/lru_dict-1.3.0-cp311-cp311-win32.whl", hash = "sha256:a690c23fc353681ed8042d9fe8f48f0fb79a57b9a45daea2f0be1eef8a1a4aa4", size = 12533, upload-time = "2023-11-06T01:39:05.838Z" }, 43 + { url = "https://files.pythonhosted.org/packages/ce/fe/1f12f33513310860ec6d722709ec4ad8256d9dcc3385f6ae2a244e6e66f5/lru_dict-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:efd3f4e0385d18f20f7ea6b08af2574c1bfaa5cb590102ef1bee781bdfba84bc", size = 13651, upload-time = "2023-11-06T01:39:06.871Z" }, 44 + { url = "https://files.pythonhosted.org/packages/fc/5c/385f080747eb3083af87d8e4c9068f3c4cab89035f6982134889940dafd8/lru_dict-1.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c279068f68af3b46a5d649855e1fb87f5705fe1f744a529d82b2885c0e1fc69d", size = 17174, upload-time = "2023-11-06T01:39:07.923Z" }, 45 + { url = "https://files.pythonhosted.org/packages/3c/de/5ef2ed75ce55d7059d1b96177ba04fa7ee1f35564f97bdfcd28fccfbe9d2/lru_dict-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:350e2233cfee9f326a0d7a08e309372d87186565e43a691b120006285a0ac549", size = 10742, upload-time = "2023-11-06T01:39:08.871Z" }, 46 + { url = "https://files.pythonhosted.org/packages/ca/05/f69a6abb0062d2cf2ce0aaf0284b105b97d1da024ca6d3d0730e6151242e/lru_dict-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4eafb188a84483b3231259bf19030859f070321b00326dcb8e8c6cbf7db4b12f", size = 11079, upload-time = "2023-11-06T01:39:09.766Z" }, 47 + { url = "https://files.pythonhosted.org/packages/ea/59/cf891143abe58a455b8eaa9175f0e80f624a146a2bf9a1ca842ee0ef930a/lru_dict-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73593791047e36b37fdc0b67b76aeed439fcea80959c7d46201240f9ec3b2563", size = 32469, upload-time = "2023-11-06T01:39:11.091Z" }, 48 + { url = "https://files.pythonhosted.org/packages/59/88/d5976e9f70107ce11e45d93c6f0c2d5eaa1fc30bb3c8f57525eda4510dff/lru_dict-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1958cb70b9542773d6241974646e5410e41ef32e5c9e437d44040d59bd80daf2", size = 33496, upload-time = "2023-11-06T01:39:12.463Z" }, 49 + { url = "https://files.pythonhosted.org/packages/6c/f8/94d6e910d54fc1fa05c0ee1cd608c39401866a18cf5e5aff238449b33c11/lru_dict-1.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc1cd3ed2cee78a47f11f3b70be053903bda197a873fd146e25c60c8e5a32cd6", size = 29914, upload-time = "2023-11-06T01:39:13.395Z" }, 50 + { url = "https://files.pythonhosted.org/packages/ca/b9/9db79780c8a3cfd66bba6847773061e5cf8a3746950273b9985d47bbfe53/lru_dict-1.3.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82eb230d48eaebd6977a92ddaa6d788f14cf4f4bcf5bbffa4ddfd60d051aa9d4", size = 32241, upload-time = "2023-11-06T01:39:14.612Z" }, 51 + { url = "https://files.pythonhosted.org/packages/9b/b6/08a623019daec22a40c4d6d2c40851dfa3d129a53b2f9469db8eb13666c1/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5ad659cbc349d0c9ba8e536b5f40f96a70c360f43323c29f4257f340d891531c", size = 37320, upload-time = "2023-11-06T01:39:15.875Z" }, 52 + { url = "https://files.pythonhosted.org/packages/70/0b/d3717159c26155ff77679cee1b077d22e1008bf45f19921e193319cd8e46/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ba490b8972531d153ac0d4e421f60d793d71a2f4adbe2f7740b3c55dce0a12f1", size = 35054, upload-time = "2023-11-06T01:39:17.063Z" }, 53 + { url = "https://files.pythonhosted.org/packages/04/74/f2ae00de7c27984a19b88d2b09ac877031c525b01199d7841ec8fa657fd6/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:c0131351b8a7226c69f1eba5814cbc9d1d8daaf0fdec1ae3f30508e3de5262d4", size = 38613, upload-time = "2023-11-06T01:39:18.136Z" }, 54 + { url = "https://files.pythonhosted.org/packages/5a/0b/e30236aafe31b4247aa9ae61ba8aac6dde75c3ea0e47a8fb7eef53f6d5ce/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0e88dba16695f17f41701269fa046197a3fd7b34a8dba744c8749303ddaa18df", size = 37143, upload-time = "2023-11-06T01:39:19.571Z" }, 55 + { url = "https://files.pythonhosted.org/packages/1c/28/b59bcebb8d76ba8147a784a8be7eab6a4ad3395b9236e73740ff675a5a52/lru_dict-1.3.0-cp312-cp312-win32.whl", hash = "sha256:6ffaf595e625b388babc8e7d79b40f26c7485f61f16efe76764e32dce9ea17fc", size = 12653, upload-time = "2023-11-06T01:39:20.574Z" }, 56 + { url = "https://files.pythonhosted.org/packages/bd/18/06d9710cb0a0d3634f8501e4bdcc07abe64a32e404d82895a6a36fab97f6/lru_dict-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf9da32ef2582434842ab6ba6e67290debfae72771255a8e8ab16f3e006de0aa", size = 13811, upload-time = "2023-11-06T01:39:21.599Z" }, 57 + ] 58 + 59 + [[package]] 60 + name = "more-itertools" 61 + version = "10.8.0" 62 + source = { registry = "https://pypi.org/simple" } 63 + sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } 64 + wheels = [ 65 + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, 66 + ] 67 + 68 + [[package]] 69 + name = "mst-test-suite" 70 + version = "0.1.0" 71 + source = { virtual = "." } 72 + dependencies = [ 73 + { name = "atmst" }, 74 + { name = "cbrrr" }, 75 + ] 76 + 77 + [package.metadata] 78 + requires-dist = [ 79 + { name = "atmst", specifier = ">=0.0.6" }, 80 + { name = "cbrrr", specifier = ">=1.0.1" }, 81 + ]
+5 -6
packages/utilities/mst/package.json
··· 2 2 "type": "module", 3 3 "name": "@atcute/mst", 4 4 "version": "0.1.0", 5 - "description": "atproto mst manipulation utilities", 5 + "description": "atproto MST manipulation utilities", 6 6 "keywords": [ 7 7 "atproto", 8 8 "repo", 9 - "mst", 10 - "dasl", 11 - "car" 9 + "mst" 12 10 ], 13 11 "license": "0BSD", 14 12 "repository": { ··· 26 24 }, 27 25 "sideEffects": false, 28 26 "scripts": { 27 + "generate-tests": "cd mst-test-suite && mise exec -- uv run python scripts/generate_exhaustive_cars.py", 29 28 "build": "tsc --project tsconfig.build.json", 30 29 "test": "vitest run", 31 30 "prepublish": "rm -rf dist; pnpm run build" ··· 33 32 "devDependencies": { 34 33 "@atcute/car": "workspace:^", 35 34 "@vitest/coverage-v8": "^3.2.4", 35 + "valibot": "^1.1.0", 36 36 "vitest": "^3.2.4" 37 37 }, 38 38 "dependencies": { 39 39 "@atcute/cbor": "workspace:^", 40 40 "@atcute/cid": "workspace:^", 41 41 "@atcute/uint8array": "workspace:^", 42 - "@atcute/varint": "workspace:^", 43 - "@badrap/valita": "^0.4.6" 42 + "@atcute/varint": "workspace:^" 44 43 } 45 44 }
+3 -3
pnpm-lock.yaml
··· 747 747 '@atcute/varint': 748 748 specifier: workspace:^ 749 749 version: link:../varint 750 - '@badrap/valita': 751 - specifier: ^0.4.6 752 - version: 0.4.6 753 750 devDependencies: 754 751 '@atcute/car': 755 752 specifier: workspace:^ ··· 757 754 '@vitest/coverage-v8': 758 755 specifier: ^3.2.4 759 756 version: 3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4) 757 + valibot: 758 + specifier: ^1.1.0 759 + version: 1.1.0(typescript@5.9.2) 760 760 vitest: 761 761 specifier: ^3.2.4 762 762 version: 3.2.4(@types/node@24.3.0)(@vitest/browser@3.2.4)(tsx@4.20.6)(yaml@2.8.0)