[DEPRECATED] Go implementation of plcbundle

cmd index

Changed files
+426 -335
cmd
plcbundle
+4
cmd/plcbundle/commands/common.go
··· 35 35 GetDIDIndexStats() map[string]interface{} 36 36 GetDIDIndex() *didindex.Manager 37 37 BuildDIDIndex(ctx context.Context, progress func(int, int)) error 38 + GetDIDOperations(ctx context.Context, did string, verbose bool) ([]plcclient.PLCOperation, error) 38 39 GetDIDOperationsWithLocations(ctx context.Context, did string, verbose bool) ([]bundle.PLCOperationWithLocation, error) 39 40 GetDIDOperationsFromMempool(did string) ([]plcclient.PLCOperation, error) 40 41 GetLatestDIDOperation(ctx context.Context, did string) (*plcclient.PLCOperation, error) ··· 43 44 ResolveDID(ctx context.Context, did string) (*bundle.ResolveDIDResult, error) 44 45 RunSyncOnce(ctx context.Context, config *internalsync.SyncLoopConfig, verbose bool) (int, error) 45 46 RunSyncLoop(ctx context.Context, config *internalsync.SyncLoopConfig) error 47 + GetBundleIndex() didindex.BundleIndexProvider 48 + ScanDirectoryParallel(workers int, progressCallback func(current, total int, bytesProcessed int64)) (*bundle.DirectoryScanResult, error) 49 + LoadBundleForDIDIndex(ctx context.Context, bundleNumber int) (*didindex.BundleData, error) 46 50 } 47 51 48 52 // PLCOperationWithLocation wraps operation with location info
+130
cmd/plcbundle/commands/did.go
··· 978 978 979 979 return nil 980 980 } 981 + 982 + // ============================================================================ 983 + // Shared Helper Functions (used by both DID and legacy index commands) 984 + // ============================================================================ 985 + 986 + func outputLookupJSON(did string, opsWithLoc []PLCOperationWithLocation, mempoolOps []plcclient.PLCOperation, totalElapsed, lookupElapsed, mempoolElapsed time.Duration) error { 987 + output := map[string]interface{}{ 988 + "found": true, 989 + "did": did, 990 + "timing": map[string]interface{}{ 991 + "total_ms": totalElapsed.Milliseconds(), 992 + "lookup_ms": lookupElapsed.Milliseconds(), 993 + "mempool_ms": mempoolElapsed.Milliseconds(), 994 + }, 995 + "bundled": make([]map[string]interface{}, 0), 996 + "mempool": make([]map[string]interface{}, 0), 997 + } 998 + 999 + for _, owl := range opsWithLoc { 1000 + output["bundled"] = append(output["bundled"].([]map[string]interface{}), map[string]interface{}{ 1001 + "bundle": owl.Bundle, 1002 + "position": owl.Position, 1003 + "cid": owl.Operation.CID, 1004 + "nullified": owl.Operation.IsNullified(), 1005 + "created_at": owl.Operation.CreatedAt.Format(time.RFC3339Nano), 1006 + }) 1007 + } 1008 + 1009 + for _, op := range mempoolOps { 1010 + output["mempool"] = append(output["mempool"].([]map[string]interface{}), map[string]interface{}{ 1011 + "cid": op.CID, 1012 + "nullified": op.IsNullified(), 1013 + "created_at": op.CreatedAt.Format(time.RFC3339Nano), 1014 + }) 1015 + } 1016 + 1017 + data, _ := json.MarshalIndent(output, "", " ") 1018 + fmt.Println(string(data)) 1019 + 1020 + return nil 1021 + } 1022 + 1023 + func displayLookupResults(did string, opsWithLoc []PLCOperationWithLocation, mempoolOps []plcclient.PLCOperation, totalElapsed, lookupElapsed, mempoolElapsed time.Duration, verbose bool, stats map[string]interface{}) error { 1024 + nullifiedCount := 0 1025 + for _, owl := range opsWithLoc { 1026 + if owl.Operation.IsNullified() { 1027 + nullifiedCount++ 1028 + } 1029 + } 1030 + 1031 + totalOps := len(opsWithLoc) + len(mempoolOps) 1032 + activeOps := len(opsWithLoc) - nullifiedCount + len(mempoolOps) 1033 + 1034 + fmt.Printf("═══════════════════════════════════════════════════════════════\n") 1035 + fmt.Printf(" DID Lookup Results\n") 1036 + fmt.Printf("═══════════════════════════════════════════════════════════════\n\n") 1037 + fmt.Printf("DID: %s\n\n", did) 1038 + 1039 + fmt.Printf("Summary\n───────\n") 1040 + fmt.Printf(" Total operations: %d\n", totalOps) 1041 + fmt.Printf(" Active operations: %d\n", activeOps) 1042 + if nullifiedCount > 0 { 1043 + fmt.Printf(" Nullified: %d\n", nullifiedCount) 1044 + } 1045 + if len(opsWithLoc) > 0 { 1046 + fmt.Printf(" Bundled: %d\n", len(opsWithLoc)) 1047 + } 1048 + if len(mempoolOps) > 0 { 1049 + fmt.Printf(" Mempool: %d\n", len(mempoolOps)) 1050 + } 1051 + fmt.Printf("\n") 1052 + 1053 + fmt.Printf("Performance\n───────────\n") 1054 + fmt.Printf(" Index lookup: %s\n", lookupElapsed) 1055 + fmt.Printf(" Mempool check: %s\n", mempoolElapsed) 1056 + fmt.Printf(" Total time: %s\n\n", totalElapsed) 1057 + 1058 + // Show operations 1059 + if len(opsWithLoc) > 0 { 1060 + fmt.Printf("Bundled Operations (%d total)\n", len(opsWithLoc)) 1061 + fmt.Printf("══════════════════════════════════════════════════════════════\n\n") 1062 + 1063 + for i, owl := range opsWithLoc { 1064 + op := owl.Operation 1065 + status := "✓ Active" 1066 + if op.IsNullified() { 1067 + status = "✗ Nullified" 1068 + } 1069 + 1070 + fmt.Printf("Operation %d [Bundle %06d, Position %04d]\n", i+1, owl.Bundle, owl.Position) 1071 + fmt.Printf(" CID: %s\n", op.CID) 1072 + fmt.Printf(" Created: %s\n", op.CreatedAt.Format("2006-01-02 15:04:05.000 MST")) 1073 + fmt.Printf(" Status: %s\n", status) 1074 + 1075 + if verbose && !op.IsNullified() { 1076 + showOperationDetails(&op) 1077 + } 1078 + 1079 + fmt.Printf("\n") 1080 + } 1081 + } 1082 + 1083 + fmt.Printf("═══════════════════════════════════════════════════════════════\n") 1084 + fmt.Printf("✓ Lookup complete in %s\n", totalElapsed) 1085 + if stats["exists"].(bool) { 1086 + fmt.Printf(" Method: DID index (fast)\n") 1087 + } else { 1088 + fmt.Printf(" Method: Full scan (slow)\n") 1089 + } 1090 + fmt.Printf("═══════════════════════════════════════════════════════════════\n") 1091 + 1092 + return nil 1093 + } 1094 + 1095 + func showOperationDetails(op *plcclient.PLCOperation) { 1096 + if opData, err := op.GetOperationData(); err == nil && opData != nil { 1097 + if opType, ok := opData["type"].(string); ok { 1098 + fmt.Printf(" Type: %s\n", opType) 1099 + } 1100 + 1101 + if handle, ok := opData["handle"].(string); ok { 1102 + fmt.Printf(" Handle: %s\n", handle) 1103 + } else if aka, ok := opData["alsoKnownAs"].([]interface{}); ok && len(aka) > 0 { 1104 + if akaStr, ok := aka[0].(string); ok { 1105 + handle := strings.TrimPrefix(akaStr, "at://") 1106 + fmt.Printf(" Handle: %s\n", handle) 1107 + } 1108 + } 1109 + } 1110 + }
+291 -334
cmd/plcbundle/commands/index.go
··· 1 + // repo/cmd/plcbundle/commands/index.go 1 2 package commands 2 3 3 4 import ( 4 5 "context" 5 6 "fmt" 6 - "os" 7 - "strings" 8 7 "time" 9 - 10 - flag "github.com/spf13/pflag" 11 8 12 9 "github.com/goccy/go-json" 10 + "github.com/spf13/cobra" 13 11 "tangled.org/atscan.net/plcbundle/cmd/plcbundle/ui" 14 - "tangled.org/atscan.net/plcbundle/internal/plcclient" 15 12 ) 16 13 17 - // IndexCommand handles the index subcommand 18 - func IndexCommand(args []string) error { 19 - if len(args) < 1 { 20 - printIndexUsage() 21 - return fmt.Errorf("subcommand required") 14 + func NewIndexCommand() *cobra.Command { 15 + cmd := &cobra.Command{ 16 + Use: "index", 17 + Short: "DID index management", 18 + Long: `DID index management operations 19 + 20 + Manage the DID position index which maps DIDs to their bundle locations. 21 + This index enables fast O(1) DID lookups and is required for DID 22 + resolution and query operations.`, 23 + 24 + Example: ` # Build DID position index 25 + plcbundle index build 26 + 27 + # Repair DID index (rebuild from bundles) 28 + plcbundle index repair 29 + 30 + # Show DID index statistics 31 + plcbundle index stats 32 + 33 + # Verify DID index integrity 34 + plcbundle index verify`, 22 35 } 23 36 24 - subcommand := args[0] 37 + cmd.AddCommand(newIndexBuildCommand()) 38 + cmd.AddCommand(newIndexRepairCommand()) 39 + cmd.AddCommand(newIndexStatsCommand()) 40 + cmd.AddCommand(newIndexVerifyCommand()) 25 41 26 - switch subcommand { 27 - case "build": 28 - return indexBuild(args[1:]) 29 - case "stats": 30 - return indexStats(args[1:]) 31 - case "lookup": 32 - return indexLookup(args[1:]) 33 - case "resolve": 34 - return indexResolve(args[1:]) 35 - default: 36 - printIndexUsage() 37 - return fmt.Errorf("unknown index subcommand: %s", subcommand) 38 - } 42 + return cmd 39 43 } 40 44 41 - func printIndexUsage() { 42 - fmt.Printf(`Usage: plcbundle index <command> [options] 45 + // ============================================================================ 46 + // INDEX BUILD - Build DID position index 47 + // ============================================================================ 48 + 49 + func newIndexBuildCommand() *cobra.Command { 50 + var force bool 51 + 52 + cmd := &cobra.Command{ 53 + Use: "build", 54 + Short: "Build DID position index", 55 + Long: `Build DID position index from bundles 43 56 44 - Commands: 45 - build Build DID index from bundles 46 - stats Show index statistics 47 - lookup Lookup a specific DID 48 - resolve Resolve DID to current document 57 + Creates a sharded index mapping each DID to its bundle locations, 58 + enabling fast O(1) DID lookups. Required for DID resolution. 59 + 60 + The index is built incrementally and auto-updates as new bundles 61 + are added. Use --force to rebuild from scratch.`, 49 62 50 - Examples: 63 + Example: ` # Build index 51 64 plcbundle index build 52 - plcbundle index stats 53 - plcbundle index lookup did:plc:524tuhdhh3m7li5gycdn6boe 54 - plcbundle index resolve did:plc:524tuhdhh3m7li5gycdn6boe 55 - `) 56 - } 65 + 66 + # Force rebuild from scratch 67 + plcbundle index build --force`, 57 68 58 - func indexBuild(args []string) error { 59 - fs := flag.NewFlagSet("index build", flag.ExitOnError) 60 - force := fs.Bool("force", false, "rebuild even if index exists") 69 + RunE: func(cmd *cobra.Command, args []string) error { 70 + mgr, dir, err := getManager(&ManagerOptions{Cmd: cmd}) 71 + if err != nil { 72 + return err 73 + } 74 + defer mgr.Close() 61 75 62 - if err := fs.Parse(args); err != nil { 63 - return err 64 - } 76 + stats := mgr.GetDIDIndexStats() 77 + if stats["exists"].(bool) && !force { 78 + fmt.Printf("DID index already exists (use --force to rebuild)\n") 79 + fmt.Printf("Directory: %s\n", dir) 80 + fmt.Printf("Total DIDs: %s\n", formatNumber(int(stats["total_dids"].(int64)))) 81 + return nil 82 + } 65 83 66 - mgr, dir, err := getManager(nil) 67 - if err != nil { 68 - return err 69 - } 70 - defer mgr.Close() 84 + index := mgr.GetIndex() 85 + bundleCount := index.Count() 71 86 72 - stats := mgr.GetDIDIndexStats() 73 - if stats["exists"].(bool) && !*force { 74 - fmt.Printf("DID index already exists (use --force to rebuild)\n") 75 - fmt.Printf("Directory: %s\n", dir) 76 - fmt.Printf("Total DIDs: %d\n", stats["total_dids"]) 77 - return nil 78 - } 87 + if bundleCount == 0 { 88 + fmt.Printf("No bundles to index\n") 89 + return nil 90 + } 79 91 80 - fmt.Printf("Building DID index in: %s\n", dir) 92 + fmt.Printf("Building DID index in: %s\n", dir) 93 + fmt.Printf("Indexing %d bundles...\n\n", bundleCount) 81 94 82 - index := mgr.GetIndex() 83 - bundleCount := index.Count() 95 + progress := ui.NewProgressBar(bundleCount) 96 + start := time.Now() 97 + ctx := context.Background() 84 98 85 - if bundleCount == 0 { 86 - fmt.Printf("No bundles to index\n") 87 - return nil 88 - } 99 + err = mgr.BuildDIDIndex(ctx, func(current, total int) { 100 + progress.Set(current) 101 + }) 89 102 90 - fmt.Printf("Indexing %d bundles...\n\n", bundleCount) 103 + progress.Finish() 91 104 92 - progress := ui.NewProgressBar(bundleCount) 93 - start := time.Now() 94 - ctx := context.Background() 105 + if err != nil { 106 + return fmt.Errorf("build failed: %w", err) 107 + } 95 108 96 - err = mgr.BuildDIDIndex(ctx, func(current, total int) { 97 - progress.Set(current) 98 - }) 109 + elapsed := time.Since(start) 110 + stats = mgr.GetDIDIndexStats() 99 111 100 - progress.Finish() 112 + fmt.Printf("\n✓ DID index built in %s\n", elapsed.Round(time.Millisecond)) 113 + fmt.Printf(" Total DIDs: %s\n", formatNumber(int(stats["total_dids"].(int64)))) 114 + fmt.Printf(" Shards: %d\n", stats["shard_count"]) 115 + fmt.Printf(" Location: %s/.plcbundle/\n", dir) 101 116 102 - if err != nil { 103 - return fmt.Errorf("error building index: %w", err) 117 + return nil 118 + }, 104 119 } 105 120 106 - elapsed := time.Since(start) 107 - stats = mgr.GetDIDIndexStats() 108 - 109 - fmt.Printf("\n✓ DID index built in %s\n", elapsed.Round(time.Millisecond)) 110 - fmt.Printf(" Total DIDs: %s\n", formatNumber(int(stats["total_dids"].(int64)))) 111 - fmt.Printf(" Shards: %d\n", stats["shard_count"]) 112 - fmt.Printf(" Location: %s/.plcbundle/\n", dir) 121 + cmd.Flags().BoolVar(&force, "force", false, "Rebuild even if index exists") 113 122 114 - return nil 123 + return cmd 115 124 } 116 125 117 - func indexStats(args []string) error { 118 - mgr, dir, err := getManager(nil) 119 - if err != nil { 120 - return err 121 - } 122 - defer mgr.Close() 126 + // ============================================================================ 127 + // INDEX REPAIR - Repair/rebuild DID index 128 + // ============================================================================ 123 129 124 - stats := mgr.GetDIDIndexStats() 130 + func newIndexRepairCommand() *cobra.Command { 131 + cmd := &cobra.Command{ 132 + Use: "repair", 133 + Aliases: []string{"rebuild"}, 134 + Short: "Repair DID index", 135 + Long: `Repair DID index by rebuilding from bundles 125 136 126 - if !stats["exists"].(bool) { 127 - fmt.Printf("DID index does not exist\n") 128 - fmt.Printf("Run: plcbundle index build\n") 129 - return nil 130 - } 137 + Rebuilds the DID index from scratch and verifies consistency. 138 + Use this when: 139 + • DID index is corrupted 140 + • Index is out of sync with bundles 141 + • After manual bundle operations 142 + • Upgrade to new index version`, 131 143 132 - indexedDIDs := stats["indexed_dids"].(int64) 133 - mempoolDIDs := stats["mempool_dids"].(int64) 134 - totalDIDs := stats["total_dids"].(int64) 144 + Example: ` # Repair DID index 145 + plcbundle index repair 135 146 136 - fmt.Printf("\nDID Index Statistics\n") 137 - fmt.Printf("════════════════════\n\n") 138 - fmt.Printf(" Location: %s/.plcbundle/\n", dir) 147 + # Verbose output 148 + plcbundle index repair -v`, 139 149 140 - if mempoolDIDs > 0 { 141 - fmt.Printf(" Indexed DIDs: %s (in bundles)\n", formatNumber(int(indexedDIDs))) 142 - fmt.Printf(" Mempool DIDs: %s (not yet bundled)\n", formatNumber(int(mempoolDIDs))) 143 - fmt.Printf(" Total DIDs: %s\n", formatNumber(int(totalDIDs))) 144 - } else { 145 - fmt.Printf(" Total DIDs: %s\n", formatNumber(int(totalDIDs))) 146 - } 150 + RunE: func(cmd *cobra.Command, args []string) error { 151 + verbose, _ := cmd.Root().PersistentFlags().GetBool("verbose") 147 152 148 - fmt.Printf(" Shard count: %d\n", stats["shard_count"]) 149 - fmt.Printf(" Last bundle: %06d\n", stats["last_bundle"]) 150 - fmt.Printf(" Updated: %s\n\n", stats["updated_at"].(time.Time).Format("2006-01-02 15:04:05")) 151 - fmt.Printf(" Cached shards: %d / %d\n", stats["cached_shards"], stats["cache_limit"]) 153 + mgr, dir, err := getManager(&ManagerOptions{Cmd: cmd}) 154 + if err != nil { 155 + return err 156 + } 157 + defer mgr.Close() 152 158 153 - if cachedList, ok := stats["cache_order"].([]int); ok && len(cachedList) > 0 { 154 - fmt.Printf(" Hot shards: ") 155 - for i, shard := range cachedList { 156 - if i > 0 { 157 - fmt.Printf(", ") 159 + stats := mgr.GetDIDIndexStats() 160 + if !stats["exists"].(bool) { 161 + fmt.Printf("DID index does not exist\n") 162 + fmt.Printf("Use: plcbundle index build\n") 163 + return nil 158 164 } 159 - if i >= 10 { 160 - fmt.Printf("... (+%d more)", len(cachedList)-10) 161 - break 165 + 166 + fmt.Printf("Repairing DID index in: %s\n\n", dir) 167 + 168 + index := mgr.GetIndex() 169 + bundleCount := index.Count() 170 + 171 + if bundleCount == 0 { 172 + fmt.Printf("No bundles to index\n") 173 + return nil 162 174 } 163 - fmt.Printf("%02x", shard) 164 - } 165 - fmt.Printf("\n") 166 - } 167 175 168 - fmt.Printf("\n") 169 - return nil 170 - } 176 + fmt.Printf("Rebuilding index from %d bundles...\n\n", bundleCount) 171 177 172 - func indexLookup(args []string) error { 173 - fs := flag.NewFlagSet("index lookup", flag.ExitOnError) 174 - verbose := fs.Bool("v", false, "verbose debug output") 175 - showJSON := fs.Bool("json", false, "output as JSON") 178 + var progress *ui.ProgressBar 179 + if !verbose { 180 + progress = ui.NewProgressBar(bundleCount) 181 + } 176 182 177 - if err := fs.Parse(args); err != nil { 178 - return err 179 - } 183 + start := time.Now() 184 + ctx := context.Background() 180 185 181 - if fs.NArg() < 1 { 182 - return fmt.Errorf("usage: plcbundle index lookup <did> [-v] [--json]") 183 - } 186 + err = mgr.BuildDIDIndex(ctx, func(current, total int) { 187 + if progress != nil { 188 + progress.Set(current) 189 + } else if current%100 == 0 || current == total { 190 + fmt.Printf("Progress: %d/%d (%.1f%%) \r", 191 + current, total, float64(current)/float64(total)*100) 192 + } 193 + }) 184 194 185 - did := fs.Arg(0) 195 + if progress != nil { 196 + progress.Finish() 197 + } 186 198 187 - mgr, _, err := getManager(nil) 188 - if err != nil { 189 - return err 190 - } 191 - defer mgr.Close() 199 + if err != nil { 200 + return fmt.Errorf("repair failed: %w", err) 201 + } 192 202 193 - stats := mgr.GetDIDIndexStats() 194 - if !stats["exists"].(bool) { 195 - fmt.Fprintf(os.Stderr, "⚠️ DID index does not exist. Run: plcbundle index build\n") 196 - fmt.Fprintf(os.Stderr, " Falling back to full scan (this will be slow)...\n\n") 197 - } 203 + // Verify consistency 204 + fmt.Printf("\nVerifying consistency...\n") 205 + if err := mgr.GetDIDIndex().VerifyAndRepairIndex(ctx, mgr); err != nil { 206 + return fmt.Errorf("verification failed: %w", err) 207 + } 198 208 199 - if !*showJSON { 200 - fmt.Printf("Looking up: %s\n", did) 201 - if *verbose { 202 - fmt.Printf("Verbose mode: enabled\n") 203 - } 204 - fmt.Printf("\n") 205 - } 209 + elapsed := time.Since(start) 210 + stats = mgr.GetDIDIndexStats() 206 211 207 - totalStart := time.Now() 208 - ctx := context.Background() 212 + fmt.Printf("\n✓ DID index repaired in %s\n", elapsed.Round(time.Millisecond)) 213 + fmt.Printf(" Total DIDs: %s\n", formatNumber(int(stats["total_dids"].(int64)))) 214 + fmt.Printf(" Shards: %d\n", stats["shard_count"]) 215 + fmt.Printf(" Last bundle: %06d\n", stats["last_bundle"]) 209 216 210 - // Lookup operations 211 - lookupStart := time.Now() 212 - opsWithLoc, err := mgr.GetDIDOperationsWithLocations(ctx, did, *verbose) 213 - if err != nil { 214 - return err 217 + return nil 218 + }, 215 219 } 216 - lookupElapsed := time.Since(lookupStart) 217 220 218 - // Check mempool 219 - mempoolStart := time.Now() 220 - mempoolOps, err := mgr.GetDIDOperationsFromMempool(did) 221 - if err != nil { 222 - return fmt.Errorf("error checking mempool: %w", err) 223 - } 224 - mempoolElapsed := time.Since(mempoolStart) 221 + return cmd 222 + } 225 223 226 - totalElapsed := time.Since(totalStart) 224 + // ============================================================================ 225 + // INDEX STATS - Show DID index statistics 226 + // ============================================================================ 227 227 228 - if len(opsWithLoc) == 0 && len(mempoolOps) == 0 { 229 - if *showJSON { 230 - fmt.Println("{\"found\": false, \"operations\": []}") 231 - } else { 232 - fmt.Printf("DID not found (searched in %s)\n", totalElapsed) 233 - } 234 - return nil 235 - } 228 + func newIndexStatsCommand() *cobra.Command { 229 + var showJSON bool 236 230 237 - if *showJSON { 238 - return outputLookupJSON(did, opsWithLoc, mempoolOps, totalElapsed, lookupElapsed, mempoolElapsed) 239 - } 231 + cmd := &cobra.Command{ 232 + Use: "stats", 233 + Aliases: []string{"info"}, 234 + Short: "Show DID index statistics", 235 + Long: `Show DID index statistics 240 236 241 - return displayLookupResults(did, opsWithLoc, mempoolOps, totalElapsed, lookupElapsed, mempoolElapsed, *verbose, stats) 242 - } 237 + Displays DID index information including total DIDs indexed, 238 + shard distribution, cache statistics, and coverage.`, 243 239 244 - func indexResolve(args []string) error { 245 - fs := flag.NewFlagSet("index resolve", flag.ExitOnError) 246 - verbose := fs.BoolP("verbose", "v", false, "verbose timing breakdown") 240 + Example: ` # Show statistics 241 + plcbundle index stats 247 242 248 - if err := fs.Parse(args); err != nil { 249 - return err 250 - } 243 + # JSON output 244 + plcbundle index stats --json`, 251 245 252 - if fs.NArg() < 1 { 253 - return fmt.Errorf("usage: plcbundle index resolve <did> [-v]") 254 - } 246 + RunE: func(cmd *cobra.Command, args []string) error { 247 + mgr, dir, err := getManager(&ManagerOptions{Cmd: cmd}) 248 + if err != nil { 249 + return err 250 + } 251 + defer mgr.Close() 255 252 256 - did := fs.Arg(0) 253 + stats := mgr.GetDIDIndexStats() 257 254 258 - mgr, _, err := getManager(nil) 259 - if err != nil { 260 - return err 261 - } 262 - defer mgr.Close() 255 + if showJSON { 256 + data, _ := json.MarshalIndent(stats, "", " ") 257 + fmt.Println(string(data)) 258 + return nil 259 + } 263 260 264 - ctx := context.Background() 265 - fmt.Fprintf(os.Stderr, "Resolving: %s\n", did) 261 + if !stats["exists"].(bool) { 262 + fmt.Printf("DID index does not exist\n") 263 + fmt.Printf("Run: plcbundle index build\n") 264 + return nil 265 + } 266 266 267 - if *verbose { 268 - mgr.GetDIDIndex().SetVerbose(true) 269 - } 267 + indexedDIDs := stats["indexed_dids"].(int64) 268 + mempoolDIDs := stats["mempool_dids"].(int64) 269 + totalDIDs := stats["total_dids"].(int64) 270 270 271 - // Use unified resolution method with metrics 272 - result, err := mgr.ResolveDID(ctx, did) 273 - if err != nil { 274 - return err 275 - } 271 + fmt.Printf("\nDID Index Statistics\n") 272 + fmt.Printf("════════════════════\n\n") 273 + fmt.Printf(" Location: %s/.plcbundle/\n", dir) 276 274 277 - // Display timing metrics 278 - if result.Source == "mempool" { 279 - fmt.Fprintf(os.Stderr, "Mempool check: %s (✓ found in mempool)\n", result.MempoolTime) 280 - fmt.Fprintf(os.Stderr, "Total: %s (resolved from mempool)\n\n", result.TotalTime) 281 - } else { 282 - fmt.Fprintf(os.Stderr, "Mempool check: %s (not found)\n", result.MempoolTime) 283 - fmt.Fprintf(os.Stderr, "Index lookup: %s (shard access)\n", result.IndexTime) 284 - fmt.Fprintf(os.Stderr, "Operation load: %s (bundle %d, pos %d)\n", 285 - result.LoadOpTime, result.BundleNumber, result.Position) 286 - fmt.Fprintf(os.Stderr, "Total: %s\n", result.TotalTime) 275 + if mempoolDIDs > 0 { 276 + fmt.Printf(" Indexed DIDs: %s (in bundles)\n", formatNumber(int(indexedDIDs))) 277 + fmt.Printf(" Mempool DIDs: %s (not yet bundled)\n", formatNumber(int(mempoolDIDs))) 278 + fmt.Printf(" Total DIDs: %s\n", formatNumber(int(totalDIDs))) 279 + } else { 280 + fmt.Printf(" Total DIDs: %s\n", formatNumber(int(totalDIDs))) 281 + } 287 282 288 - // Verbose timing breakdown 289 - if *verbose { 290 - fmt.Fprintf(os.Stderr, "\nTiming breakdown:\n") 291 - fmt.Fprintf(os.Stderr, " Mempool: %6s (%5.1f%%)\n", 292 - result.MempoolTime, float64(result.MempoolTime)/float64(result.TotalTime)*100) 293 - fmt.Fprintf(os.Stderr, " Index: %6s (%5.1f%%)\n", 294 - result.IndexTime, float64(result.IndexTime)/float64(result.TotalTime)*100) 295 - fmt.Fprintf(os.Stderr, " Load op: %6s (%5.1f%%)\n", 296 - result.LoadOpTime, float64(result.LoadOpTime)/float64(result.TotalTime)*100) 297 - } 298 - fmt.Fprintf(os.Stderr, "\n") 299 - } 283 + fmt.Printf(" Shard count: %d\n", stats["shard_count"]) 284 + fmt.Printf(" Last bundle: %06d\n", stats["last_bundle"]) 285 + fmt.Printf(" Updated: %s\n\n", stats["updated_at"].(time.Time).Format("2006-01-02 15:04:05")) 300 286 301 - // Output document to stdout 302 - data, _ := json.MarshalIndent(result.Document, "", " ") 303 - fmt.Println(string(data)) 287 + fmt.Printf(" Cached shards: %d / %d\n", stats["cached_shards"], stats["cache_limit"]) 304 288 305 - return nil 306 - } 289 + if cachedList, ok := stats["cache_order"].([]int); ok && len(cachedList) > 0 { 290 + fmt.Printf(" Hot shards: ") 291 + for i, shard := range cachedList { 292 + if i > 0 { 293 + fmt.Printf(", ") 294 + } 295 + if i >= 10 { 296 + fmt.Printf("... (+%d more)", len(cachedList)-10) 297 + break 298 + } 299 + fmt.Printf("%02x", shard) 300 + } 301 + fmt.Printf("\n") 302 + } 307 303 308 - func outputLookupJSON(did string, opsWithLoc []PLCOperationWithLocation, mempoolOps []plcclient.PLCOperation, totalElapsed, lookupElapsed, mempoolElapsed time.Duration) error { 309 - output := map[string]interface{}{ 310 - "found": true, 311 - "did": did, 312 - "timing": map[string]interface{}{ 313 - "total_ms": totalElapsed.Milliseconds(), 314 - "lookup_ms": lookupElapsed.Milliseconds(), 315 - "mempool_ms": mempoolElapsed.Milliseconds(), 304 + fmt.Printf("\n") 305 + return nil 316 306 }, 317 - "bundled": make([]map[string]interface{}, 0), 318 - "mempool": make([]map[string]interface{}, 0), 319 307 } 320 308 321 - for _, owl := range opsWithLoc { 322 - output["bundled"] = append(output["bundled"].([]map[string]interface{}), map[string]interface{}{ 323 - "bundle": owl.Bundle, 324 - "position": owl.Position, 325 - "cid": owl.Operation.CID, 326 - "nullified": owl.Operation.IsNullified(), 327 - "created_at": owl.Operation.CreatedAt.Format(time.RFC3339Nano), 328 - }) 329 - } 309 + cmd.Flags().BoolVar(&showJSON, "json", false, "Output as JSON") 330 310 331 - for _, op := range mempoolOps { 332 - output["mempool"] = append(output["mempool"].([]map[string]interface{}), map[string]interface{}{ 333 - "cid": op.CID, 334 - "nullified": op.IsNullified(), 335 - "created_at": op.CreatedAt.Format(time.RFC3339Nano), 336 - }) 337 - } 338 - 339 - data, _ := json.MarshalIndent(output, "", " ") 340 - fmt.Println(string(data)) 341 - 342 - return nil 311 + return cmd 343 312 } 344 313 345 - func displayLookupResults(did string, opsWithLoc []PLCOperationWithLocation, mempoolOps []plcclient.PLCOperation, totalElapsed, lookupElapsed, mempoolElapsed time.Duration, verbose bool, stats map[string]interface{}) error { 346 - nullifiedCount := 0 347 - for _, owl := range opsWithLoc { 348 - if owl.Operation.IsNullified() { 349 - nullifiedCount++ 350 - } 351 - } 314 + // ============================================================================ 315 + // INDEX VERIFY - Verify DID index integrity 316 + // ============================================================================ 352 317 353 - totalOps := len(opsWithLoc) + len(mempoolOps) 354 - activeOps := len(opsWithLoc) - nullifiedCount + len(mempoolOps) 318 + func newIndexVerifyCommand() *cobra.Command { 319 + var verbose bool 355 320 356 - fmt.Printf("═══════════════════════════════════════════════════════════════\n") 357 - fmt.Printf(" DID Lookup Results\n") 358 - fmt.Printf("═══════════════════════════════════════════════════════════════\n\n") 359 - fmt.Printf("DID: %s\n\n", did) 321 + cmd := &cobra.Command{ 322 + Use: "verify", 323 + Aliases: []string{"check"}, 324 + Short: "Verify DID index integrity", 325 + Long: `Verify DID index integrity and consistency 360 326 361 - fmt.Printf("Summary\n───────\n") 362 - fmt.Printf(" Total operations: %d\n", totalOps) 363 - fmt.Printf(" Active operations: %d\n", activeOps) 364 - if nullifiedCount > 0 { 365 - fmt.Printf(" Nullified: %d\n", nullifiedCount) 366 - } 367 - if len(opsWithLoc) > 0 { 368 - fmt.Printf(" Bundled: %d\n", len(opsWithLoc)) 369 - } 370 - if len(mempoolOps) > 0 { 371 - fmt.Printf(" Mempool: %d\n", len(mempoolOps)) 372 - } 373 - fmt.Printf("\n") 327 + Checks the DID index for consistency with bundles: 328 + • Index version is current 329 + • All bundles are indexed 330 + • Shard files are valid 331 + • No corruption detected 374 332 375 - fmt.Printf("Performance\n───────────\n") 376 - fmt.Printf(" Index lookup: %s\n", lookupElapsed) 377 - fmt.Printf(" Mempool check: %s\n", mempoolElapsed) 378 - fmt.Printf(" Total time: %s\n\n", totalElapsed) 333 + Automatically repairs minor issues.`, 379 334 380 - // Show operations 381 - if len(opsWithLoc) > 0 { 382 - fmt.Printf("Bundled Operations (%d total)\n", len(opsWithLoc)) 383 - fmt.Printf("══════════════════════════════════════════════════════════════\n\n") 335 + Example: ` # Verify DID index 336 + plcbundle index verify 384 337 385 - for i, owl := range opsWithLoc { 386 - op := owl.Operation 387 - status := "✓ Active" 388 - if op.IsNullified() { 389 - status = "✗ Nullified" 338 + # Verbose output 339 + plcbundle index verify -v`, 340 + 341 + RunE: func(cmd *cobra.Command, args []string) error { 342 + mgr, dir, err := getManager(&ManagerOptions{Cmd: cmd}) 343 + if err != nil { 344 + return err 390 345 } 346 + defer mgr.Close() 391 347 392 - fmt.Printf("Operation %d [Bundle %06d, Position %04d]\n", i+1, owl.Bundle, owl.Position) 393 - fmt.Printf(" CID: %s\n", op.CID) 394 - fmt.Printf(" Created: %s\n", op.CreatedAt.Format("2006-01-02 15:04:05.000 MST")) 395 - fmt.Printf(" Status: %s\n", status) 348 + stats := mgr.GetDIDIndexStats() 396 349 397 - if verbose && !op.IsNullified() { 398 - showOperationDetails(&op) 350 + if !stats["exists"].(bool) { 351 + fmt.Printf("DID index does not exist\n") 352 + fmt.Printf("Run: plcbundle index build\n") 353 + return nil 399 354 } 400 355 401 - fmt.Printf("\n") 402 - } 403 - } 356 + fmt.Printf("Verifying DID index in: %s\n\n", dir) 404 357 405 - fmt.Printf("═══════════════════════════════════════════════════════════════\n") 406 - fmt.Printf("✓ Lookup complete in %s\n", totalElapsed) 407 - if stats["exists"].(bool) { 408 - fmt.Printf(" Method: DID index (fast)\n") 409 - } else { 410 - fmt.Printf(" Method: Full scan (slow)\n") 411 - } 412 - fmt.Printf("═══════════════════════════════════════════════════════════════\n") 358 + ctx := context.Background() 413 359 414 - return nil 415 - } 360 + if verbose { 361 + fmt.Printf("Index version: %d\n", mgr.GetDIDIndex().GetConfig().Version) 362 + fmt.Printf("Total DIDs: %s\n", formatNumber(int(stats["total_dids"].(int64)))) 363 + fmt.Printf("Shards: %d\n", stats["shard_count"]) 364 + fmt.Printf("Last bundle: %06d\n\n", stats["last_bundle"]) 365 + } 416 366 417 - func showOperationDetails(op *plcclient.PLCOperation) { 418 - if opData, err := op.GetOperationData(); err == nil && opData != nil { 419 - if opType, ok := opData["type"].(string); ok { 420 - fmt.Printf(" Type: %s\n", opType) 421 - } 367 + fmt.Printf("Checking consistency with bundles...\n") 422 368 423 - if handle, ok := opData["handle"].(string); ok { 424 - fmt.Printf(" Handle: %s\n", handle) 425 - } else if aka, ok := opData["alsoKnownAs"].([]interface{}); ok && len(aka) > 0 { 426 - if akaStr, ok := aka[0].(string); ok { 427 - handle := strings.TrimPrefix(akaStr, "at://") 428 - fmt.Printf(" Handle: %s\n", handle) 369 + if err := mgr.GetDIDIndex().VerifyAndRepairIndex(ctx, mgr); err != nil { 370 + fmt.Printf("\n✗ DID index verification failed\n") 371 + fmt.Printf(" Error: %v\n", err) 372 + return fmt.Errorf("verification failed: %w", err) 429 373 } 430 - } 374 + 375 + stats = mgr.GetDIDIndexStats() 376 + 377 + fmt.Printf("\n✓ DID index is valid\n") 378 + fmt.Printf(" Total DIDs: %s\n", formatNumber(int(stats["total_dids"].(int64)))) 379 + fmt.Printf(" Shards: %d\n", stats["shard_count"]) 380 + fmt.Printf(" Last bundle: %06d\n", stats["last_bundle"]) 381 + 382 + return nil 383 + }, 431 384 } 385 + 386 + cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output") 387 + 388 + return cmd 432 389 }
+1 -1
cmd/plcbundle/main.go
··· 64 64 65 65 // Namespaced commands 66 66 cmd.AddCommand(commands.NewDIDCommand()) 67 - //cmd.AddCommand(commands.NewIndexCommand()) 67 + cmd.AddCommand(commands.NewIndexCommand()) 68 68 cmd.AddCommand(commands.NewMempoolCommand()) 69 69 /*cmd.AddCommand(commands.NewDetectorCommand()) 70 70