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

Configure Feed

Select the types of activity you want to include in your feed.

refactor(mst): flesh it out even more!

Mary bebc93c4 e17e21a2

+527 -188
+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)