A Transparent and Verifiable Way to Sync the AT Protocol's PLC Directory

fix warnings

+238 -147
+6 -8
bundle/manager.go
··· 369 369 } 370 370 371 371 // loadBundleFromDisk loads a bundle from disk 372 - func (m *Manager) loadBundleFromDisk(ctx context.Context, bundleNumber int) (*Bundle, error) { 372 + func (m *Manager) loadBundleFromDisk(_ context.Context, bundleNumber int) (*Bundle, error) { 373 373 // Get metadata from index 374 374 meta, err := m.index.GetBundle(bundleNumber) 375 375 if err != nil { ··· 1321 1321 // Find latest non-nullified location 1322 1322 var latestLoc *didindex.OpLocation 1323 1323 for i := range locations { 1324 - if locations[i].Nullified { 1324 + if locations[i].Nullified() { 1325 1325 continue 1326 1326 } 1327 - if latestLoc == nil || 1328 - locations[i].Bundle > latestLoc.Bundle || 1329 - (locations[i].Bundle == latestLoc.Bundle && locations[i].Position > latestLoc.Position) { 1327 + if latestLoc == nil || locations[i].IsAfter(*latestLoc) { 1330 1328 latestLoc = &locations[i] 1331 1329 } 1332 1330 } ··· 1337 1335 1338 1336 // STEP 3: Load operation 1339 1337 opStart := time.Now() 1340 - op, err := m.LoadOperation(ctx, int(latestLoc.Bundle), int(latestLoc.Position)) 1338 + op, err := m.LoadOperation(ctx, latestLoc.BundleInt(), latestLoc.PositionInt()) 1341 1339 result.LoadOpTime = time.Since(opStart) 1342 1340 1343 1341 if err != nil { 1344 1342 return nil, fmt.Errorf("failed to load operation: %w", err) 1345 1343 } 1346 1344 1347 - result.BundleNumber = int(latestLoc.Bundle) 1348 - result.Position = int(latestLoc.Position) 1345 + result.BundleNumber = latestLoc.BundleInt() 1346 + result.Position = latestLoc.PositionInt() 1349 1347 1350 1348 // STEP 4: Resolve document 1351 1349 doc, err := plcclient.ResolveDIDDocument(did, []plcclient.PLCOperation{*op})
+1 -1
cmd/plcbundle/commands/detector.go
··· 62 62 `) 63 63 } 64 64 65 - func detectorList(args []string) error { 65 + func detectorList(_ []string) error { 66 66 registry := detector.DefaultRegistry() 67 67 detectors := registry.List() 68 68
+3 -3
cmd/plcbundle/commands/did.go
··· 795 795 return nil 796 796 } 797 797 798 - func batchLookup(mgr BundleManager, dids []string, output *os.File, workers int) error { 798 + func batchLookup(mgr BundleManager, dids []string, output *os.File, _ int) error { 799 799 progress := ui.NewProgressBar(len(dids)) 800 800 ctx := context.Background() 801 801 ··· 855 855 return nil 856 856 } 857 857 858 - func batchResolve(mgr BundleManager, dids []string, output *os.File, workers int) error { 858 + func batchResolve(mgr BundleManager, dids []string, output *os.File, _ int) error { 859 859 progress := ui.NewProgressBar(len(dids)) 860 860 ctx := context.Background() 861 861 ··· 900 900 return nil 901 901 } 902 902 903 - func batchExport(mgr BundleManager, dids []string, output *os.File, workers int) error { 903 + func batchExport(mgr BundleManager, dids []string, output *os.File, _ int) error { 904 904 progress := ui.NewProgressBar(len(dids)) 905 905 ctx := context.Background() 906 906
+12 -8
cmd/plcbundle/commands/export.go
··· 24 24 } 25 25 26 26 if !*all && *bundles == "" { 27 - return fmt.Errorf("usage: plcbundle export --bundles <number|range> [options]\n" + 28 - " or: plcbundle export --all [options]\n\n" + 29 - "Examples:\n" + 30 - " plcbundle export --bundles 42\n" + 31 - " plcbundle export --bundles 1-100\n" + 32 - " plcbundle export --all\n" + 33 - " plcbundle export --all --count 50000\n" + 34 - " plcbundle export --bundles 42 | jq .") 27 + fmt.Fprint(os.Stderr, `usage: plcbundle export --bundles <number|range> [options] 28 + or: plcbundle export --all [options] 29 + 30 + Examples: 31 + plcbundle export --bundles 42 32 + plcbundle export --bundles 1-100 33 + plcbundle export --all 34 + plcbundle export --all --count 50000 35 + plcbundle export --bundles 42 | jq . 36 + 37 + `) 38 + return fmt.Errorf("missing required flag: --bundles or --all") 35 39 } 36 40 37 41 mgr, _, err := getManager(&ManagerOptions{Cmd: nil})
+6 -6
cmd/plcbundle/commands/log.go
··· 197 197 // Display bundles 198 198 for i, meta := range displayBundles { 199 199 if opts.oneline { 200 - displayBundleOneLine(w, meta, opts.showHashes, useColor, colorBundleNum, colorHash, colorDate, colorAge, colorSize, colorReset) 200 + displayBundleOneLine(w, meta, opts.showHashes, colorBundleNum, colorHash, colorDate, colorAge, colorSize, colorReset) 201 201 } else { 202 - displayBundleDetailed(w, meta, opts.showHashes, useColor, colorBundleNum, colorHash, colorDate, colorAge, colorSize, colorDim, colorReset) 202 + displayBundleDetailed(w, meta, opts.showHashes, colorBundleNum, colorHash, colorDate, colorAge, colorSize, colorDim, colorReset) 203 203 204 204 // Add separator between bundles (except last) 205 205 if i < len(displayBundles)-1 { ··· 212 212 // Summary footer 213 213 if !opts.oneline && len(displayBundles) > 0 { 214 214 fmt.Fprintf(w, "\n") 215 - displayLogSummary(w, allBundles, displayBundles, opts.last, useColor, colorHeader, colorReset) 215 + displayLogSummary(w, allBundles, displayBundles, opts.last, colorHeader, colorReset) 216 216 } 217 217 } 218 218 219 - func displayBundleOneLine(w io.Writer, meta *bundleindex.BundleMetadata, showHashes bool, useColor bool, colorBundle, colorHash, colorDate, colorAge, colorSize, colorReset string) { 219 + func displayBundleOneLine(w io.Writer, meta *bundleindex.BundleMetadata, showHashes bool, colorBundle, colorHash, colorDate, colorAge, colorSize, colorReset string) { 220 220 age := time.Since(meta.EndTime) 221 221 ageStr := formatDurationShort(age) 222 222 ··· 236 236 colorSize, formatBytes(meta.CompressedSize), colorReset) 237 237 } 238 238 239 - func displayBundleDetailed(w io.Writer, meta *bundleindex.BundleMetadata, showHashes bool, useColor bool, colorBundle, colorHash, colorDate, colorAge, colorSize, colorDim, colorReset string) { 239 + func displayBundleDetailed(w io.Writer, meta *bundleindex.BundleMetadata, showHashes bool, colorBundle, colorHash, colorDate, colorAge, colorSize, colorDim, colorReset string) { 240 240 fmt.Fprintf(w, "%sBundle %06d%s\n", colorBundle, meta.BundleNumber, colorReset) 241 241 242 242 // Timestamp and age ··· 282 282 } 283 283 } 284 284 285 - func displayLogSummary(w io.Writer, allBundles, displayedBundles []*bundleindex.BundleMetadata, limit int, useColor bool, colorHeader, colorReset string) { 285 + func displayLogSummary(w io.Writer, allBundles, displayedBundles []*bundleindex.BundleMetadata, limit int, colorHeader, colorReset string) { 286 286 first := displayedBundles[0] 287 287 last := displayedBundles[len(displayedBundles)-1] 288 288
+1 -1
cmd/plcbundle/commands/mempool.go
··· 83 83 return cmd 84 84 } 85 85 86 - func mempoolStatus(cmd *cobra.Command, args []string) error { 86 + func mempoolStatus(cmd *cobra.Command, _ []string) error { 87 87 verbose, _ := cmd.Flags().GetBool("verbose") 88 88 if cmd.Parent() != nil { 89 89 // Called as subcommand, check parent's verbose flag
-17
cmd/plcbundle/commands/rollback.go
··· 527 527 fmt.Printf(" plcbundle index build\n\n") 528 528 } 529 529 } 530 - 531 - // Validation helpers 532 - 533 - // validateRollbackSafety performs additional safety checks 534 - func validateRollbackSafety(mgr BundleManager, plan *rollbackPlan) error { 535 - // Check for chain integrity issues 536 - if len(plan.toKeep) > 1 { 537 - // Verify the target bundle exists and has valid hash 538 - lastKeep := plan.toKeep[len(plan.toKeep)-1] 539 - if lastKeep.Hash == "" { 540 - return fmt.Errorf("target bundle %06d has no chain hash - may be corrupted", 541 - lastKeep.BundleNumber) 542 - } 543 - } 544 - 545 - return nil 546 - }
+1 -2
cmd/plcbundle/commands/stream.go
··· 230 230 } 231 231 232 232 type streamLogger struct { 233 - quiet bool 234 - verbose bool 233 + quiet bool 235 234 } 236 235 237 236 func (l *streamLogger) Printf(format string, v ...interface{}) {
+31 -41
internal/didindex/builder.go
··· 20 20 } 21 21 22 22 // add adds a location to the shard 23 - func (sb *ShardBuilder) add(identifier string, bundle uint16, position uint16, nullified bool) { 23 + func (sb *ShardBuilder) add(identifier string, loc OpLocation) { 24 24 sb.mu.Lock() 25 25 defer sb.mu.Unlock() 26 26 27 - sb.entries[identifier] = append(sb.entries[identifier], OpLocation{ 28 - Bundle: bundle, 29 - Position: position, 30 - Nullified: nullified, 31 - }) 27 + sb.entries[identifier] = append(sb.entries[identifier], loc) 28 + } 29 + 30 + // updateAndSaveConfig updates config with new values and saves atomically 31 + func (dim *Manager) updateAndSaveConfig(totalDIDs int64, lastBundle int) error { 32 + dim.config.TotalDIDs = totalDIDs 33 + dim.config.LastBundle = lastBundle 34 + dim.config.Version = DIDINDEX_VERSION 35 + dim.config.Format = "binary_v4" 36 + dim.config.UpdatedAt = time.Now().UTC() 37 + 38 + return dim.saveIndexConfig() 32 39 } 33 40 34 41 // BuildIndexFromScratch builds index with controlled memory usage ··· 92 99 93 100 shardNum := dim.calculateShard(identifier) 94 101 95 - // Write entry: [24 bytes ID][2 bytes bundle][2 bytes pos][1 byte nullified] 96 - entry := make([]byte, 29) 102 + // Write entry: [24 bytes ID][4 bytes packed OpLocation] 103 + entry := make([]byte, 28) 97 104 copy(entry[0:24], identifier) 98 - binary.LittleEndian.PutUint16(entry[24:26], uint16(meta.BundleNumber)) 99 - binary.LittleEndian.PutUint16(entry[26:28], uint16(pos)) 100 105 101 - // Store nullified flag 102 - if op.IsNullified() { 103 - entry[28] = 1 104 - } else { 105 - entry[28] = 0 106 - } 106 + // Create packed OpLocation (includes nullified bit) 107 + loc := NewOpLocation(uint16(meta.BundleNumber), uint16(pos), op.IsNullified()) 108 + binary.LittleEndian.PutUint32(entry[24:28], uint32(loc)) 107 109 108 110 if _, err := tempShards[shardNum].Write(entry); err != nil { 109 111 dim.logger.Printf("Warning: failed to write to temp shard %02x: %v", shardNum, err) ··· 135 137 totalDIDs += count 136 138 } 137 139 138 - dim.config.TotalDIDs = totalDIDs 139 - dim.config.LastBundle = bundles[len(bundles)-1].BundleNumber 140 - 141 - if err := dim.saveIndexConfig(); err != nil { 140 + if err := dim.updateAndSaveConfig(totalDIDs, bundles[len(bundles)-1].BundleNumber); err != nil { 142 141 return fmt.Errorf("failed to save config: %w", err) 143 142 } 144 143 ··· 165 164 return 0, nil 166 165 } 167 166 168 - // Parse entries (29 bytes each) 169 - entryCount := len(data) / 29 170 - if len(data)%29 != 0 { 171 - return 0, fmt.Errorf("corrupted temp shard: size not multiple of 29") 167 + // Parse entries (28 bytes each) 168 + entryCount := len(data) / 28 169 + if len(data)%28 != 0 { 170 + return 0, fmt.Errorf("corrupted temp shard: size not multiple of 28") 172 171 } 173 172 174 173 type tempEntry struct { 175 174 identifier string 176 - bundle uint16 177 - position uint16 178 - nullified bool 175 + location OpLocation // ← Single packed value 179 176 } 180 177 181 178 entries := make([]tempEntry, entryCount) 182 179 for i := 0; i < entryCount; i++ { 183 - offset := i * 29 180 + offset := i * 28 // ← 28 bytes 184 181 entries[i] = tempEntry{ 185 182 identifier: string(data[offset : offset+24]), 186 - bundle: binary.LittleEndian.Uint16(data[offset+24 : offset+26]), 187 - position: binary.LittleEndian.Uint16(data[offset+26 : offset+28]), 188 - nullified: data[offset+28] != 0, 183 + location: OpLocation(binary.LittleEndian.Uint32(data[offset+24 : offset+28])), 189 184 } 190 185 } 191 186 ··· 200 195 // Group by DID 201 196 builder := newShardBuilder() 202 197 for _, entry := range entries { 203 - builder.add(entry.identifier, entry.bundle, entry.position, entry.nullified) 198 + builder.add(entry.identifier, entry.location) 204 199 } 205 200 206 201 // Free entries ··· 240 235 shardOps[shardNum] = make(map[string][]OpLocation) 241 236 } 242 237 243 - shardOps[shardNum][identifier] = append(shardOps[shardNum][identifier], OpLocation{ 244 - Bundle: uint16(bundle.BundleNumber), 245 - Position: uint16(pos), 246 - Nullified: op.IsNullified(), 247 - }) 238 + loc := NewOpLocation(uint16(bundle.BundleNumber), uint16(pos), op.IsNullified()) 239 + shardOps[shardNum][identifier] = append(shardOps[shardNum][identifier], loc) 248 240 } 249 241 250 242 groupDuration := time.Since(groupStart) ··· 349 341 // STEP 4: Update config 350 342 configStart := time.Now() 351 343 352 - dim.config.TotalDIDs += deltaCount 353 - dim.config.LastBundle = bundle.BundleNumber 354 - 355 - if err := dim.saveIndexConfig(); err != nil { 344 + newTotal := dim.config.TotalDIDs + deltaCount 345 + if err := dim.updateAndSaveConfig(newTotal, bundle.BundleNumber); err != nil { 356 346 return fmt.Errorf("failed to save config: %w", err) 357 347 } 358 348
+12 -12
internal/didindex/lookup.go
··· 34 34 // Filter nullified 35 35 var validLocations []OpLocation 36 36 for _, loc := range locations { 37 - if !loc.Nullified { 37 + if !loc.Nullified() { 38 38 validLocations = append(validLocations, loc) 39 39 } 40 40 } ··· 46 46 47 47 if len(validLocations) == 1 { 48 48 loc := validLocations[0] 49 - op, err := provider.LoadOperation(ctx, int(loc.Bundle), int(loc.Position)) 49 + op, err := provider.LoadOperation(ctx, loc.BundleInt(), loc.PositionInt()) 50 50 if err != nil { 51 51 return nil, err 52 52 } ··· 56 56 // For multiple operations: group by bundle to minimize bundle loads 57 57 bundleMap := make(map[uint16][]uint16) 58 58 for _, loc := range validLocations { 59 - bundleMap[loc.Bundle] = append(bundleMap[loc.Bundle], loc.Position) 59 + bundleMap[loc.Bundle()] = append(bundleMap[loc.Bundle()], loc.Position()) 60 60 } 61 61 62 62 if dim.verbose { ··· 133 133 // Group by bundle 134 134 bundleMap := make(map[uint16][]OpLocation) 135 135 for _, loc := range locations { 136 - bundleMap[loc.Bundle] = append(bundleMap[loc.Bundle], loc) 136 + bundleMap[loc.Bundle()] = append(bundleMap[loc.Bundle()], loc) 137 137 } 138 138 139 139 if dim.verbose { ··· 149 149 } 150 150 151 151 for _, loc := range locs { 152 - if int(loc.Position) >= len(bundle.Operations) { 152 + if loc.PositionInt() >= len(bundle.Operations) { 153 153 continue 154 154 } 155 155 156 - op := bundle.Operations[loc.Position] 156 + op := bundle.Operations[loc.Position()] 157 157 results = append(results, OpLocationWithOperation{ 158 158 Operation: op, 159 - Bundle: int(loc.Bundle), 160 - Position: int(loc.Position), 159 + Bundle: loc.BundleInt(), 160 + Position: loc.PositionInt(), 161 161 }) 162 162 } 163 163 } ··· 196 196 // Find latest non-nullified location 197 197 var latestLoc *OpLocation 198 198 for i := range locations { 199 - if locations[i].Nullified { 199 + if locations[i].Nullified() { 200 200 continue 201 201 } 202 202 203 203 if latestLoc == nil { 204 204 latestLoc = &locations[i] 205 205 } else { 206 - if locations[i].Bundle > latestLoc.Bundle || 207 - (locations[i].Bundle == latestLoc.Bundle && locations[i].Position > latestLoc.Position) { 206 + if locations[i].Bundle() > latestLoc.Bundle() || 207 + (locations[i].Bundle() == latestLoc.Bundle() && locations[i].Position() > latestLoc.Position()) { 208 208 latestLoc = &locations[i] 209 209 } 210 210 } ··· 215 215 } 216 216 217 217 // Load ONLY the specific operation (efficient!) 218 - return provider.LoadOperation(ctx, int(latestLoc.Bundle), int(latestLoc.Position)) 218 + return provider.LoadOperation(ctx, latestLoc.BundleInt(), latestLoc.PositionInt()) 219 219 }
+40 -40
internal/didindex/manager.go
··· 25 25 config, _ := loadIndexConfig(configPath) 26 26 if config == nil { 27 27 config = &Config{ 28 - Version: DIDINDEX_VERSION, 29 - Format: "binary_v1", 28 + Version: DIDINDEX_VERSION, // Will be 4 29 + Format: "binary_v4", // Update format name 30 30 ShardCount: DID_SHARD_COUNT, 31 31 UpdatedAt: time.Now().UTC(), 32 32 } 33 + } else if config.Version < DIDINDEX_VERSION { 34 + // Auto-trigger rebuild on version mismatch 35 + logger.Printf("DID index version outdated (v%d, need v%d) - rebuild required", 36 + config.Version, DIDINDEX_VERSION) 33 37 } 34 38 35 39 return &Manager{ ··· 43 47 config: config, 44 48 logger: logger, 45 49 } 46 - } 47 - 48 - // Add helper to ensure directories when actually writing 49 - func (dim *Manager) ensureDirectories() error { 50 - return os.MkdirAll(dim.shardDir, 0755) 51 50 } 52 51 53 52 // Close unmaps all shards and cleans up ··· 274 273 275 274 // searchShard performs optimized binary search using prefix index 276 275 func (dim *Manager) searchShard(shard *mmapShard, identifier string) []OpLocation { 277 - if shard.data == nil || len(shard.data) < 1056 { 276 + if len(shard.data) < 1056 { 278 277 return nil 279 278 } 280 279 ··· 449 448 // Read locations 450 449 locations := make([]OpLocation, count) 451 450 for i := 0; i < int(count); i++ { 452 - if offset+5 > len(data) { 451 + if offset+4 > len(data) { // ← 4 bytes now 453 452 return locations[:i] 454 453 } 455 454 456 - bundle := binary.LittleEndian.Uint16(data[offset : offset+2]) 457 - position := binary.LittleEndian.Uint16(data[offset+2 : offset+4]) 458 - nullified := data[offset+4] != 0 455 + // Read packed uint32 456 + packed := binary.LittleEndian.Uint32(data[offset : offset+4]) 457 + locations[i] = OpLocation(packed) 459 458 460 - locations[i] = OpLocation{ 461 - Bundle: bundle, 462 - Position: position, 463 - Nullified: nullified, 464 - } 465 - 466 - offset += 5 459 + offset += 4 // ← 4 bytes 467 460 } 468 461 469 462 return locations ··· 660 653 for i, id := range identifiers { 661 654 offsetTable[i] = uint32(currentOffset) 662 655 locations := builder.entries[id] 663 - entrySize := DID_IDENTIFIER_LEN + 2 + (len(locations) * 5) 656 + entrySize := DID_IDENTIFIER_LEN + 2 + (len(locations) * 4) // ← 4 bytes 664 657 currentOffset += entrySize 665 658 } 666 659 ··· 700 693 offset += 2 701 694 702 695 for _, loc := range locations { 703 - binary.LittleEndian.PutUint16(buf[offset:offset+2], loc.Bundle) 704 - binary.LittleEndian.PutUint16(buf[offset+2:offset+4], loc.Position) 705 - 706 - if loc.Nullified { 707 - buf[offset+4] = 1 708 - } else { 709 - buf[offset+4] = 0 710 - } 711 - 712 - offset += 5 696 + // Write packed uint32 (global position + nullified bit) 697 + binary.LittleEndian.PutUint32(buf[offset:offset+4], uint32(loc)) 698 + offset += 4 // ← 4 bytes per location 713 699 } 714 700 } 715 701 ··· 724 710 725 711 entryCount := binary.LittleEndian.Uint32(data[9:13]) 726 712 727 - var offsetTableStart int 728 - offsetTableStart = 1056 713 + offsetTableStart := 1056 729 714 730 715 // Start reading entries after offset table 731 716 offset := offsetTableStart + (int(entryCount) * 4) ··· 745 730 746 731 // Read locations 747 732 locations := make([]OpLocation, locCount) 733 + 734 + // Check version to determine format 735 + version := binary.LittleEndian.Uint32(data[4:8]) 736 + 748 737 for j := 0; j < int(locCount); j++ { 749 - if offset+5 > len(data) { 750 - break 751 - } 738 + if version >= 4 { 739 + // New format: 4-byte packed uint32 740 + if offset+4 > len(data) { 741 + break 742 + } 743 + packed := binary.LittleEndian.Uint32(data[offset : offset+4]) 744 + locations[j] = OpLocation(packed) 745 + offset += 4 746 + } else { 747 + // Old format: 5-byte separate fields (for migration) 748 + if offset+5 > len(data) { 749 + break 750 + } 751 + bundle := binary.LittleEndian.Uint16(data[offset : offset+2]) 752 + position := binary.LittleEndian.Uint16(data[offset+2 : offset+4]) 753 + nullified := data[offset+4] != 0 752 754 753 - locations[j] = OpLocation{ 754 - Bundle: binary.LittleEndian.Uint16(data[offset : offset+2]), 755 - Position: binary.LittleEndian.Uint16(data[offset+2 : offset+4]), 756 - Nullified: data[offset+4] != 0, 755 + // Convert to new format 756 + locations[j] = NewOpLocation(bundle, position, nullified) 757 + offset += 5 757 758 } 758 - offset += 5 759 759 } 760 760 761 761 builder.entries[identifier] = locations
+58
internal/didindex/manager_test.go
··· 1 + package didindex_test 2 + 3 + import ( 4 + "testing" 5 + 6 + "tangled.org/atscan.net/plcbundle/internal/didindex" 7 + ) 8 + 9 + func TestOpLocationPacking(t *testing.T) { 10 + tests := []struct { 11 + bundle uint16 12 + position uint16 13 + nullified bool 14 + }{ 15 + {1, 0, false}, 16 + {1, 9999, false}, 17 + {100, 5000, true}, 18 + {65535, 9999, true}, // Max values 19 + } 20 + 21 + for _, tt := range tests { 22 + loc := didindex.NewOpLocation(tt.bundle, tt.position, tt.nullified) 23 + 24 + // Test unpacking 25 + if loc.Bundle() != tt.bundle { 26 + t.Errorf("Bundle mismatch: got %d, want %d", loc.Bundle(), tt.bundle) 27 + } 28 + if loc.Position() != tt.position { 29 + t.Errorf("Position mismatch: got %d, want %d", loc.Position(), tt.position) 30 + } 31 + if loc.Nullified() != tt.nullified { 32 + t.Errorf("Nullified mismatch: got %v, want %v", loc.Nullified(), tt.nullified) 33 + } 34 + 35 + // Test global position 36 + expectedGlobal := uint32(tt.bundle)*10000 + uint32(tt.position) 37 + if loc.GlobalPosition() != expectedGlobal { 38 + t.Errorf("Global position mismatch: got %d, want %d", 39 + loc.GlobalPosition(), expectedGlobal) 40 + } 41 + } 42 + } 43 + 44 + func TestOpLocationComparison(t *testing.T) { 45 + loc1 := didindex.NewOpLocation(100, 50, false) // 1,000,050 46 + loc2 := didindex.NewOpLocation(100, 51, false) // 1,000,051 47 + loc3 := didindex.NewOpLocation(200, 30, false) // 2,000,030 48 + 49 + if !loc1.Less(loc2) { 50 + t.Error("Expected loc1 < loc2") 51 + } 52 + if !loc2.Less(loc3) { 53 + t.Error("Expected loc2 < loc3") 54 + } 55 + if loc3.Less(loc1) { 56 + t.Error("Expected loc3 > loc1") 57 + } 58 + }
+67 -8
internal/didindex/types.go
··· 17 17 18 18 // Binary format constants 19 19 DIDINDEX_MAGIC = "PLCD" 20 - DIDINDEX_VERSION = 3 20 + DIDINDEX_VERSION = 4 21 + 22 + // Format sizes 23 + LOCATION_SIZE_V3 = 5 // Old: 2+2+1 24 + LOCATION_SIZE_V4 = 4 // New: packed uint32 21 25 22 26 BUILD_BATCH_SIZE = 100 // Process 100 bundles at a time 23 27 ··· 63 67 LastBundle int `json:"last_bundle"` 64 68 } 65 69 66 - // OpLocation represents exact location of an operation 67 - type OpLocation struct { 68 - Bundle uint16 69 - Position uint16 70 - Nullified bool 71 - } 72 - 73 70 // ShardBuilder accumulates DID positions for a shard 74 71 type ShardBuilder struct { 75 72 entries map[string][]OpLocation 76 73 mu sync.Mutex 77 74 } 78 75 76 + // OpLocation represents exact location of an operation 77 + type OpLocation uint32 78 + 79 79 // OpLocationWithOperation contains an operation with its bundle/position 80 80 type OpLocationWithOperation struct { 81 81 Operation plcclient.PLCOperation 82 82 Bundle int 83 83 Position int 84 84 } 85 + 86 + func NewOpLocation(bundle, position uint16, nullified bool) OpLocation { 87 + globalPos := uint32(bundle)*10000 + uint32(position) 88 + loc := globalPos << 1 89 + if nullified { 90 + loc |= 1 91 + } 92 + return OpLocation(loc) 93 + } 94 + 95 + // Getters 96 + func (loc OpLocation) GlobalPosition() uint32 { 97 + return uint32(loc) >> 1 98 + } 99 + 100 + func (loc OpLocation) Bundle() uint16 { 101 + return uint16(loc.GlobalPosition() / 10000) 102 + } 103 + 104 + func (loc OpLocation) Position() uint16 { 105 + return uint16(loc.GlobalPosition() % 10000) 106 + } 107 + 108 + func (loc OpLocation) Nullified() bool { 109 + return (loc & 1) == 1 110 + } 111 + 112 + func (loc OpLocation) IsAfter(other OpLocation) bool { 113 + // Compare global positions directly 114 + return loc.GlobalPosition() > other.GlobalPosition() 115 + } 116 + 117 + func (loc OpLocation) IsBefore(other OpLocation) bool { 118 + return loc.GlobalPosition() < other.GlobalPosition() 119 + } 120 + 121 + func (loc OpLocation) Equals(other OpLocation) bool { 122 + // Compare entire packed value (including nullified bit) 123 + return loc == other 124 + } 125 + 126 + func (loc OpLocation) PositionEquals(other OpLocation) bool { 127 + // Compare only position (ignore nullified bit) 128 + return loc.GlobalPosition() == other.GlobalPosition() 129 + } 130 + 131 + // Convenience conversions 132 + func (loc OpLocation) BundleInt() int { 133 + return int(loc.Bundle()) 134 + } 135 + 136 + func (loc OpLocation) PositionInt() int { 137 + return int(loc.Position()) 138 + } 139 + 140 + // For sorting/comparison 141 + func (loc OpLocation) Less(other OpLocation) bool { 142 + return loc.GlobalPosition() < other.GlobalPosition() 143 + }