Skip to content

Commit 1683c53

Browse files
mikeraclaude
andcommitted
Add DLFS integration to ROOT lattice with cursor-based API
- Integrated DLFS into ROOT lattice at [:fs owner driveName] - Drive names are AString (not Keywords) for flexibility - Structure: ROOT[:fs][owner][driveName] -> SignedData<DLFSNode> - Added cursor-based constructor to DLFSLocal for lattice integration - Created DLFSCursorTest demonstrating clean detach/sync API - Created DLFSLatticeIntegrationTest for multi-owner scenarios - All 22 DLFS and lattice tests passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
1 parent f3f1be0 commit 1683c53

File tree

5 files changed

+314
-4
lines changed

5 files changed

+314
-4
lines changed

.claude/settings.local.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
"Bash(mvn clean test:*)",
77
"Bash(git add:*)",
88
"Bash(git commit:*)",
9-
"Bash(mvn test-compile exec:java:*)"
9+
"Bash(mvn test-compile exec:java:*)",
10+
"Bash(git checkout:*)",
11+
"Bash(git merge:*)",
12+
"Bash(git push:*)",
13+
"Bash(mvn test-compile:*)"
1014
]
1115
}
1216
}
Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
11
package convex.lattice;
22

33
import convex.core.cvm.Keywords;
4+
import convex.core.data.AString;
5+
import convex.core.data.AVector;
46
import convex.lattice.data.DataLattice;
7+
import convex.lattice.fs.DLFSLattice;
58
import convex.lattice.generic.KeyedLattice;
9+
import convex.lattice.generic.MapLattice;
10+
import convex.lattice.generic.OwnerLattice;
611

712
/**
813
* Static utility base for the lattice
914
*/
1015
public class Lattice {
1116

17+
/**
18+
* ROOT lattice structure with support for:
19+
* - :data - General purpose data storage
20+
* - :fs - DLFS replicated filesystem (owner -> drive name -> DLFS node)
21+
* where drive names are AString (not Keywords)
22+
*/
1223
public static KeyedLattice ROOT = KeyedLattice.create(
13-
Keywords.DATA, DataLattice.INSTANCE
24+
Keywords.DATA, DataLattice.INSTANCE,
25+
Keywords.FS, OwnerLattice.create(
26+
MapLattice.create(DLFSLattice.INSTANCE)
27+
)
1428
);
1529
}

convex-core/src/main/java/convex/lattice/fs/impl/DLFSLocal.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import convex.core.data.Hash;
2020
import convex.core.data.Index;
2121
import convex.core.data.prim.CVMLong;
22+
import convex.lattice.cursor.ACursor;
2223
import convex.lattice.cursor.Root;
2324
import convex.lattice.fs.DLFS;
2425
import convex.lattice.fs.DLFSNode;
@@ -30,14 +31,31 @@
3031
* Local DLFS Drive implementation, wrapping a lattice Cursor
3132
*/
3233
public class DLFSLocal extends DLFileSystem {
33-
34+
3435
Root<AVector<ACell>> rootCursor;
35-
36+
3637
public DLFSLocal(DLFSProvider dlfsProvider, String uriPath, AVector<ACell> rootNode) {
3738
super(dlfsProvider,uriPath,DLFSNode.getUTime(rootNode));
3839
this.rootCursor=Root.create(rootNode);
3940
}
4041

42+
/**
43+
* Creates a DLFSLocal backed by a cursor (which may be a path into a larger lattice).
44+
*
45+
* @param dlfsProvider Provider for this filesystem
46+
* @param uriPath URI path (may be null)
47+
* @param cursor Cursor pointing to the DLFS tree
48+
*/
49+
public DLFSLocal(DLFSProvider dlfsProvider, String uriPath, ACursor<AVector<ACell>> cursor) {
50+
super(dlfsProvider, uriPath, DLFSNode.getUTime(cursor.get()));
51+
if (cursor instanceof Root) {
52+
this.rootCursor = (Root<AVector<ACell>>) cursor;
53+
} else {
54+
// Wrap in Root to ensure we have a Root cursor
55+
this.rootCursor = Root.create(cursor.get());
56+
}
57+
}
58+
4159
public static DLFSLocal create(DLFSProvider provider) {
4260
return new DLFSLocal(provider,null,DLFSNode.createDirectory(CVMLong.ZERO));
4361
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package convex.lattice;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNotNull;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import org.junit.jupiter.api.Test;
8+
9+
import convex.core.crypto.AKeyPair;
10+
import convex.core.cvm.Keywords;
11+
import convex.core.data.ACell;
12+
import convex.core.data.AHashMap;
13+
import convex.core.data.AMap;
14+
import convex.core.data.AString;
15+
import convex.core.data.AVector;
16+
import convex.core.data.Blob;
17+
import convex.core.data.Index;
18+
import convex.core.data.Keyword;
19+
import convex.core.data.Maps;
20+
import convex.core.data.SignedData;
21+
import convex.core.data.Strings;
22+
import convex.core.data.prim.CVMLong;
23+
import convex.lattice.fs.DLFSNode;
24+
25+
/**
26+
* Integration tests for DLFS in the ROOT lattice structure.
27+
*
28+
* These tests verify that DLFS works correctly within the lattice framework,
29+
* including merge semantics, signature validation, and multi-owner scenarios.
30+
*/
31+
public class DLFSLatticeIntegrationTest {
32+
33+
/**
34+
* Tests basic DLFS merge within ROOT lattice structure.
35+
* Verifies that two DLFS nodes from different owners can be merged.
36+
*/
37+
@Test
38+
public void testBasicDLFSMerge() {
39+
// Create two keypairs for two different owners
40+
AKeyPair owner1Key = AKeyPair.generate();
41+
AKeyPair owner2Key = AKeyPair.generate();
42+
43+
// Owner 1 creates a DLFS filesystem with a file
44+
CVMLong timestamp1 = CVMLong.create(1000);
45+
AVector<ACell> dir1 = DLFSNode.createDirectory(timestamp1);
46+
47+
// Add a file to owner1's directory
48+
AString fileName = Strings.create("test.txt");
49+
AVector<ACell> file1 = DLFSNode.createEmptyFile(timestamp1);
50+
Blob testData1 = Blob.fromHex("48656c6c6f"); // "Hello" in hex
51+
file1 = file1.assoc(DLFSNode.POS_DATA, testData1);
52+
53+
Index<AString, AVector<ACell>> entries1 = Index.of(fileName, file1);
54+
dir1 = dir1.assoc(DLFSNode.POS_DIR, entries1);
55+
56+
// Create the drive map: {"main" -> dir1}
57+
AString driveName = Strings.create("main");
58+
AHashMap<AString, AVector<ACell>> driveMap1 = Maps.of(driveName, dir1);
59+
60+
// Sign owner1's drive map
61+
SignedData<AHashMap<AString, AVector<ACell>>> signedDriveMap1 = owner1Key.signData(driveMap1);
62+
63+
// Create the structure: owner1 -> signedDriveMap1
64+
AHashMap<ACell, SignedData<AHashMap<AString, AVector<ACell>>>> fsMap1 =
65+
Maps.of(owner1Key.getAccountKey(), signedDriveMap1);
66+
67+
// Owner 2 creates a different DLFS filesystem with a different file
68+
CVMLong timestamp2 = CVMLong.create(2000);
69+
AVector<ACell> dir2 = DLFSNode.createDirectory(timestamp2);
70+
71+
// Add a different file to owner2's directory
72+
AString fileName2 = Strings.create("other.txt");
73+
AVector<ACell> file2 = DLFSNode.createEmptyFile(timestamp2);
74+
Blob testData2 = Blob.fromHex("576f726c64"); // "World" in hex
75+
file2 = file2.assoc(DLFSNode.POS_DATA, testData2);
76+
77+
Index<AString, AVector<ACell>> entries2 = Index.of(fileName2, file2);
78+
dir2 = dir2.assoc(DLFSNode.POS_DIR, entries2);
79+
80+
// Create the drive map: {"main" -> dir2}
81+
AHashMap<AString, AVector<ACell>> driveMap2 = Maps.of(driveName, dir2);
82+
83+
// Sign owner2's drive map
84+
SignedData<AHashMap<AString, AVector<ACell>>> signedDriveMap2 = owner2Key.signData(driveMap2);
85+
86+
// Create the structure: owner2 -> signedDriveMap2
87+
AHashMap<ACell, SignedData<AHashMap<AString, AVector<ACell>>>> fsMap2 =
88+
Maps.of(owner2Key.getAccountKey(), signedDriveMap2);
89+
90+
// Get the :fs lattice from ROOT and merge
91+
ALattice<ACell> fsLattice = Lattice.ROOT.path(Keywords.FS);
92+
ACell mergedValue = fsLattice.merge(fsMap1, fsMap2);
93+
94+
// Cast and verify the merge result contains both owners
95+
assertNotNull(mergedValue, "Merged FS value should not be null");
96+
assertTrue(mergedValue instanceof AHashMap, "Merged FS should be a HashMap");
97+
98+
@SuppressWarnings("unchecked")
99+
AHashMap<ACell, SignedData<AHashMap<AString, AVector<ACell>>>> mergedFS =
100+
(AHashMap<ACell, SignedData<AHashMap<AString, AVector<ACell>>>>) mergedValue;
101+
102+
assertEquals(2, mergedFS.count(), "Should have 2 owners after merge");
103+
assertTrue(mergedFS.containsKey(owner1Key.getAccountKey()), "Should contain owner1");
104+
assertTrue(mergedFS.containsKey(owner2Key.getAccountKey()), "Should contain owner2");
105+
106+
// Verify owner1's data is intact
107+
SignedData<AHashMap<AString, AVector<ACell>>> owner1Data = mergedFS.get(owner1Key.getAccountKey());
108+
assertNotNull(owner1Data, "Owner1 data should exist");
109+
assertTrue(owner1Data.checkSignature(), "Owner1 signature should be valid");
110+
AHashMap<AString, AVector<ACell>> owner1DriveMap = owner1Data.getValue();
111+
AVector<ACell> owner1Dir = owner1DriveMap.get(driveName);
112+
assertNotNull(owner1Dir, "Owner1 drive should exist");
113+
114+
// Verify owner2's data is intact
115+
SignedData<AHashMap<AString, AVector<ACell>>> owner2Data = mergedFS.get(owner2Key.getAccountKey());
116+
assertNotNull(owner2Data, "Owner2 data should exist");
117+
assertTrue(owner2Data.checkSignature(), "Owner2 signature should be valid");
118+
AHashMap<AString, AVector<ACell>> owner2DriveMap = owner2Data.getValue();
119+
AVector<ACell> owner2Dir = owner2DriveMap.get(driveName);
120+
assertNotNull(owner2Dir, "Owner2 drive should exist");
121+
}
122+
123+
/**
124+
* Tests DLFS merge with same owner updating their drive.
125+
* Verifies that newer timestamps win during merge.
126+
*/
127+
@Test
128+
public void testSameOwnerDLFSMerge() {
129+
AKeyPair ownerKey = AKeyPair.generate();
130+
AString driveName = Strings.create("main");
131+
132+
// Node 1: Owner creates initial filesystem at timestamp 1000
133+
CVMLong timestamp1 = CVMLong.create(1000);
134+
AVector<ACell> dir1 = DLFSNode.createDirectory(timestamp1);
135+
AString fileName = Strings.create("file.txt");
136+
AVector<ACell> file1 = DLFSNode.createEmptyFile(timestamp1);
137+
file1 = file1.assoc(DLFSNode.POS_DATA, Blob.fromHex("56657273696f6e2031")); // "Version 1"
138+
139+
Index<AString, AVector<ACell>> entries1 = Index.of(fileName, file1);
140+
dir1 = dir1.assoc(DLFSNode.POS_DIR, entries1);
141+
142+
AHashMap<AString, AVector<ACell>> driveMap1 = Maps.of(driveName, dir1);
143+
SignedData<AHashMap<AString, AVector<ACell>>> signedDriveMap1 = ownerKey.signData(driveMap1);
144+
AHashMap<ACell, SignedData<AHashMap<AString, AVector<ACell>>>> fsMap1 =
145+
Maps.of(ownerKey.getAccountKey(), signedDriveMap1);
146+
147+
// Node 2: Owner updates the same file at timestamp 2000 (newer)
148+
CVMLong timestamp2 = CVMLong.create(2000);
149+
AVector<ACell> dir2 = DLFSNode.createDirectory(timestamp2);
150+
AVector<ACell> file2 = DLFSNode.createEmptyFile(timestamp2);
151+
file2 = file2.assoc(DLFSNode.POS_DATA, Blob.fromHex("56657273696f6e2032")); // "Version 2"
152+
153+
Index<AString, AVector<ACell>> entries2 = Index.of(fileName, file2);
154+
dir2 = dir2.assoc(DLFSNode.POS_DIR, entries2);
155+
156+
AHashMap<AString, AVector<ACell>> driveMap2 = Maps.of(driveName, dir2);
157+
SignedData<AHashMap<AString, AVector<ACell>>> signedDriveMap2 = ownerKey.signData(driveMap2);
158+
AHashMap<ACell, SignedData<AHashMap<AString, AVector<ACell>>>> fsMap2 =
159+
Maps.of(ownerKey.getAccountKey(), signedDriveMap2);
160+
161+
// Get the :fs lattice and merge fsMap2 (newer) into fsMap1 (older)
162+
ALattice<ACell> fsLattice = Lattice.ROOT.path(Keywords.FS);
163+
ACell mergedValue = fsLattice.merge(fsMap1, fsMap2);
164+
165+
@SuppressWarnings("unchecked")
166+
AHashMap<ACell, SignedData<AHashMap<AString, AVector<ACell>>>> mergedFS =
167+
(AHashMap<ACell, SignedData<AHashMap<AString, AVector<ACell>>>>) mergedValue;
168+
169+
// Verify only one owner exists
170+
assertEquals(1, mergedFS.count(), "Should have 1 owner");
171+
172+
SignedData<AHashMap<AString, AVector<ACell>>> ownerData = mergedFS.get(ownerKey.getAccountKey());
173+
AHashMap<AString, AVector<ACell>> ownerDriveMap = ownerData.getValue();
174+
AVector<ACell> mergedDir = ownerDriveMap.get(driveName);
175+
176+
// Check timestamp - should be the newer one (2000)
177+
CVMLong mergedTimestamp = DLFSNode.getUTime(mergedDir);
178+
assertEquals(timestamp2.longValue(), mergedTimestamp.longValue(),
179+
"Merged directory should have newer timestamp");
180+
181+
// Verify file exists with the newer content
182+
Index<AString, AVector<ACell>> mergedEntries = DLFSNode.getDirectoryEntries(mergedDir);
183+
AVector<ACell> mergedFile = mergedEntries.get(fileName);
184+
assertNotNull(mergedFile, "Merged file should exist");
185+
186+
// Verify the data is from the newer version
187+
Blob fileData = (Blob) DLFSNode.getData(mergedFile);
188+
assertEquals(Blob.fromHex("56657273696f6e2032"), fileData,
189+
"Merged file should have newer content");
190+
}
191+
192+
/**
193+
* Tests that ROOT lattice has both :data and :fs paths.
194+
*/
195+
@Test
196+
public void testRootLatticePaths() {
197+
// Verify ROOT lattice has both :data and :fs keywords
198+
assertNotNull(Lattice.ROOT, "ROOT lattice should exist");
199+
assertNotNull(Lattice.ROOT.path(Keywords.DATA), ":data sublattice should exist");
200+
assertNotNull(Lattice.ROOT.path(Keywords.FS), ":fs sublattice should exist");
201+
}
202+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package convex.lattice.fs;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
5+
6+
import java.nio.file.Files;
7+
import java.nio.file.Path;
8+
9+
import org.junit.jupiter.api.Test;
10+
11+
import convex.core.data.ACell;
12+
import convex.core.data.AHashMap;
13+
import convex.core.data.AString;
14+
import convex.core.data.AVector;
15+
import convex.core.data.Maps;
16+
import convex.core.data.Strings;
17+
import convex.core.data.prim.CVMLong;
18+
import convex.lattice.cursor.ABranchedCursor;
19+
import convex.lattice.cursor.Root;
20+
import convex.lattice.fs.impl.DLFSLocal;
21+
22+
/**
23+
* Tests for DLFS integration with lattice cursors.
24+
*
25+
* Demonstrates clean API for:
26+
* - Creating DLFS drives backed by lattice cursors
27+
* - Detaching cursors for isolated operations
28+
* - Syncing changes back to the lattice
29+
*/
30+
public class DLFSCursorTest {
31+
32+
@Test
33+
public void testDLFSWithCursorSync() throws Exception {
34+
// Create a lattice root cursor with a simple map structure
35+
// Structure: {"test" -> DLFSNode}
36+
Root<AHashMap<AString, AVector<ACell>>> root = Root.create(Maps.empty());
37+
38+
// Get branched cursor for "test" drive
39+
AString driveName = Strings.create("test");
40+
ABranchedCursor<AVector<ACell>> driveCursor = root.path(driveName);
41+
42+
// Initialize with empty DLFS tree if needed
43+
if (driveCursor.get() == null) {
44+
driveCursor.set(DLFSNode.createDirectory(CVMLong.ZERO));
45+
}
46+
47+
// Detach cursor for isolated DLFS operations
48+
ABranchedCursor<AVector<ACell>> detached = driveCursor.detach();
49+
DLFSLocal dlfs = new DLFSLocal(DLFS.provider(), null, detached);
50+
51+
// Create directory
52+
Path testDir = dlfs.getPath("/docs");
53+
Files.createDirectory(testDir);
54+
55+
// Write file
56+
Path file = dlfs.getPath("/docs/readme.txt");
57+
Files.writeString(file, "Hello from DLFS!");
58+
59+
// Sync the DLFS drive back to root lattice
60+
boolean synced = driveCursor.sync(detached);
61+
assertTrue(synced, "Sync should succeed");
62+
63+
// Verify: Create new DLFS from synced root
64+
AVector<ACell> syncedTree = root.get().get(driveName);
65+
DLFSLocal dlfs2 = new DLFSLocal(DLFS.provider(), null, syncedTree);
66+
67+
// Verify file exists and has correct content
68+
Path verifyFile = dlfs2.getPath("/docs/readme.txt");
69+
assertTrue(Files.exists(verifyFile), "File should exist after sync");
70+
assertEquals("Hello from DLFS!", Files.readString(verifyFile));
71+
}
72+
}

0 commit comments

Comments
 (0)