[DEPRECATED] Go implementation of plcbundle

cmds for dids

Changed files
+1008 -10
cmd
plcbundle
+981
cmd/plcbundle/commands/did.go
··· 1 + // repo/cmd/plcbundle/commands/did.go 2 + package commands 3 + 4 + import ( 5 + "bufio" 6 + "context" 7 + "fmt" 8 + "os" 9 + "strings" 10 + "time" 11 + 12 + "github.com/goccy/go-json" 13 + "github.com/spf13/cobra" 14 + "tangled.org/atscan.net/plcbundle/cmd/plcbundle/ui" 15 + "tangled.org/atscan.net/plcbundle/internal/plcclient" 16 + ) 17 + 18 + func NewDIDCommand() *cobra.Command { 19 + cmd := &cobra.Command{ 20 + Use: "did", 21 + Aliases: []string{"d"}, 22 + Short: "DID operations and queries", 23 + Long: `DID operations and queries 24 + 25 + Query and analyze DIDs in the bundle repository. All commands 26 + require a DID index to be built for optimal performance.`, 27 + 28 + Example: ` # Lookup all operations for a DID 29 + plcbundle did lookup did:plc:524tuhdhh3m7li5gycdn6boe 30 + 31 + # Resolve to current DID document 32 + plcbundle did resolve did:plc:524tuhdhh3m7li5gycdn6boe 33 + 34 + # Show complete audit log 35 + plcbundle did history did:plc:524tuhdhh3m7li5gycdn6boe 36 + 37 + # Show DID statistics 38 + plcbundle did stats did:plc:524tuhdhh3m7li5gycdn6boe 39 + 40 + # Batch process from file 41 + plcbundle did batch dids.txt`, 42 + } 43 + 44 + // Add subcommands 45 + cmd.AddCommand(newDIDLookupCommand()) 46 + cmd.AddCommand(newDIDResolveCommand()) 47 + cmd.AddCommand(newDIDHistoryCommand()) 48 + cmd.AddCommand(newDIDBatchCommand()) 49 + cmd.AddCommand(newDIDStatsCommand()) 50 + 51 + return cmd 52 + } 53 + 54 + // ============================================================================ 55 + // DID LOOKUP - Find all operations for a DID 56 + // ============================================================================ 57 + 58 + func newDIDLookupCommand() *cobra.Command { 59 + var ( 60 + verbose bool 61 + showJSON bool 62 + ) 63 + 64 + cmd := &cobra.Command{ 65 + Use: "lookup <did>", 66 + Aliases: []string{"find", "get"}, 67 + Short: "Find all operations for a DID", 68 + Long: `Find all operations for a DID 69 + 70 + Retrieves all operations (both bundled and mempool) for a specific DID, 71 + showing bundle locations, timestamps, and nullification status. 72 + 73 + Requires DID index to be built. If not available, will fall back to 74 + full scan (slow).`, 75 + 76 + Example: ` # Lookup DID operations 77 + plcbundle did lookup did:plc:524tuhdhh3m7li5gycdn6boe 78 + 79 + # Verbose output with timing 80 + plcbundle did lookup did:plc:524tuhdhh3m7li5gycdn6boe -v 81 + 82 + # JSON output 83 + plcbundle did lookup did:plc:524tuhdhh3m7li5gycdn6boe --json 84 + 85 + # Using alias 86 + plcbundle did find did:plc:524tuhdhh3m7li5gycdn6boe`, 87 + 88 + Args: cobra.ExactArgs(1), 89 + 90 + RunE: func(cmd *cobra.Command, args []string) error { 91 + did := args[0] 92 + 93 + mgr, _, err := getManagerFromCommand(cmd, "") 94 + if err != nil { 95 + return err 96 + } 97 + defer mgr.Close() 98 + 99 + stats := mgr.GetDIDIndexStats() 100 + if !stats["exists"].(bool) { 101 + fmt.Fprintf(os.Stderr, "⚠️ DID index not found. Run: plcbundle index build\n") 102 + fmt.Fprintf(os.Stderr, " Falling back to full scan (slow)...\n\n") 103 + } 104 + 105 + totalStart := time.Now() 106 + ctx := context.Background() 107 + 108 + // Lookup operations 109 + lookupStart := time.Now() 110 + opsWithLoc, err := mgr.GetDIDOperationsWithLocations(ctx, did, verbose) 111 + if err != nil { 112 + return err 113 + } 114 + lookupElapsed := time.Since(lookupStart) 115 + 116 + // Check mempool 117 + mempoolStart := time.Now() 118 + mempoolOps, err := mgr.GetDIDOperationsFromMempool(did) 119 + if err != nil { 120 + return fmt.Errorf("error checking mempool: %w", err) 121 + } 122 + mempoolElapsed := time.Since(mempoolStart) 123 + 124 + totalElapsed := time.Since(totalStart) 125 + 126 + if len(opsWithLoc) == 0 && len(mempoolOps) == 0 { 127 + if showJSON { 128 + fmt.Println("{\"found\": false, \"operations\": []}") 129 + } else { 130 + fmt.Printf("DID not found (searched in %s)\n", totalElapsed) 131 + } 132 + return nil 133 + } 134 + 135 + if showJSON { 136 + return outputLookupJSON(did, opsWithLoc, mempoolOps, totalElapsed, lookupElapsed, mempoolElapsed) 137 + } 138 + 139 + return displayLookupResults(did, opsWithLoc, mempoolOps, totalElapsed, lookupElapsed, mempoolElapsed, verbose, stats) 140 + }, 141 + } 142 + 143 + cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose debug output") 144 + cmd.Flags().BoolVar(&showJSON, "json", false, "Output as JSON") 145 + 146 + return cmd 147 + } 148 + 149 + // ============================================================================ 150 + // DID RESOLVE - Resolve to current document 151 + // ============================================================================ 152 + 153 + func newDIDResolveCommand() *cobra.Command { 154 + var ( 155 + verbose bool 156 + showTiming bool 157 + raw bool 158 + ) 159 + 160 + cmd := &cobra.Command{ 161 + Use: "resolve <did>", 162 + Aliases: []string{"doc", "document"}, 163 + Short: "Resolve DID to current document", 164 + Long: `Resolve DID to current W3C DID document 165 + 166 + Resolves a DID to its current state by applying all non-nullified 167 + operations in chronological order. Returns standard W3C DID document. 168 + 169 + Optimized for speed: checks mempool first, then uses DID index for 170 + O(1) lookup of latest operation.`, 171 + 172 + Example: ` # Resolve DID 173 + plcbundle did resolve did:plc:524tuhdhh3m7li5gycdn6boe 174 + 175 + # Show timing breakdown 176 + plcbundle did resolve did:plc:524tuhdhh3m7li5gycdn6boe --timing 177 + 178 + # Get raw PLC state (not W3C format) 179 + plcbundle did resolve did:plc:524tuhdhh3m7li5gycdn6boe --raw 180 + 181 + # Pipe to jq 182 + plcbundle did resolve did:plc:524tuhdhh3m7li5gycdn6boe | jq .service`, 183 + 184 + Args: cobra.ExactArgs(1), 185 + 186 + RunE: func(cmd *cobra.Command, args []string) error { 187 + did := args[0] 188 + 189 + mgr, _, err := getManagerFromCommand(cmd, "") 190 + if err != nil { 191 + return err 192 + } 193 + defer mgr.Close() 194 + 195 + ctx := context.Background() 196 + 197 + if showTiming { 198 + fmt.Fprintf(os.Stderr, "Resolving: %s\n", did) 199 + } 200 + 201 + if verbose { 202 + mgr.GetDIDIndex().SetVerbose(true) 203 + } 204 + 205 + result, err := mgr.ResolveDID(ctx, did) 206 + if err != nil { 207 + return err 208 + } 209 + 210 + // Display timing if requested 211 + if showTiming { 212 + if result.Source == "mempool" { 213 + fmt.Fprintf(os.Stderr, "Mempool check: %s (✓ found)\n", result.MempoolTime) 214 + fmt.Fprintf(os.Stderr, "Total: %s\n\n", result.TotalTime) 215 + } else { 216 + fmt.Fprintf(os.Stderr, "Mempool: %s | Index: %s | Load: %s | Total: %s\n", 217 + result.MempoolTime, result.IndexTime, result.LoadOpTime, result.TotalTime) 218 + fmt.Fprintf(os.Stderr, "Source: bundle %06d, position %d\n\n", 219 + result.BundleNumber, result.Position) 220 + } 221 + } 222 + 223 + // Output document 224 + data, _ := json.MarshalIndent(result.Document, "", " ") 225 + fmt.Println(string(data)) 226 + 227 + return nil 228 + }, 229 + } 230 + 231 + cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose debug output") 232 + cmd.Flags().BoolVar(&showTiming, "timing", false, "Show timing breakdown") 233 + cmd.Flags().BoolVar(&raw, "raw", false, "Output raw PLC state (not W3C document)") 234 + 235 + return cmd 236 + } 237 + 238 + // ============================================================================ 239 + // DID HISTORY - Show complete audit log 240 + // ============================================================================ 241 + 242 + func newDIDHistoryCommand() *cobra.Command { 243 + var ( 244 + verbose bool 245 + showJSON bool 246 + compact bool 247 + includeNullified bool 248 + ) 249 + 250 + cmd := &cobra.Command{ 251 + Use: "history <did>", 252 + Aliases: []string{"log", "audit"}, 253 + Short: "Show complete DID audit log", 254 + Long: `Show complete DID audit log 255 + 256 + Displays all operations for a DID in chronological order, showing 257 + the complete history including nullified operations. 258 + 259 + This provides a full audit trail of all changes to the DID.`, 260 + 261 + Example: ` # Show full history 262 + plcbundle did history did:plc:524tuhdhh3m7li5gycdn6boe 263 + 264 + # Include nullified operations 265 + plcbundle did history did:plc:524tuhdhh3m7li5gycdn6boe --include-nullified 266 + 267 + # Compact one-line format 268 + plcbundle did history did:plc:524tuhdhh3m7li5gycdn6boe --compact 269 + 270 + # JSON output 271 + plcbundle did history did:plc:524tuhdhh3m7li5gycdn6boe --json`, 272 + 273 + Args: cobra.ExactArgs(1), 274 + 275 + RunE: func(cmd *cobra.Command, args []string) error { 276 + did := args[0] 277 + 278 + mgr, _, err := getManagerFromCommand(cmd, "") 279 + if err != nil { 280 + return err 281 + } 282 + defer mgr.Close() 283 + 284 + ctx := context.Background() 285 + 286 + // Get all operations with locations 287 + opsWithLoc, err := mgr.GetDIDOperationsWithLocations(ctx, did, verbose) 288 + if err != nil { 289 + return err 290 + } 291 + 292 + // Get mempool operations 293 + mempoolOps, err := mgr.GetDIDOperationsFromMempool(did) 294 + if err != nil { 295 + return err 296 + } 297 + 298 + if len(opsWithLoc) == 0 && len(mempoolOps) == 0 { 299 + fmt.Fprintf(os.Stderr, "DID not found: %s\n", did) 300 + return nil 301 + } 302 + 303 + if showJSON { 304 + return outputHistoryJSON(did, opsWithLoc, mempoolOps) 305 + } 306 + 307 + return displayHistory(did, opsWithLoc, mempoolOps, compact, includeNullified) 308 + }, 309 + } 310 + 311 + cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output") 312 + cmd.Flags().BoolVar(&showJSON, "json", false, "Output as JSON") 313 + cmd.Flags().BoolVar(&compact, "compact", false, "Compact one-line format") 314 + cmd.Flags().BoolVar(&includeNullified, "include-nullified", false, "Show nullified operations") 315 + 316 + return cmd 317 + } 318 + 319 + // ============================================================================ 320 + // DID BATCH - Process multiple DIDs from file or stdin 321 + // ============================================================================ 322 + 323 + func newDIDBatchCommand() *cobra.Command { 324 + var ( 325 + action string 326 + workers int 327 + outputFile string 328 + fromStdin bool 329 + ) 330 + 331 + cmd := &cobra.Command{ 332 + Use: "batch [file]", 333 + Short: "Process multiple DIDs from file or stdin", 334 + Long: `Process multiple DIDs from file or stdin 335 + 336 + Read DIDs from a file (one per line) or stdin and perform batch operations. 337 + Supports parallel processing for better performance. 338 + 339 + Actions: 340 + lookup - Lookup all DIDs and show summary 341 + resolve - Resolve all DIDs to documents 342 + export - Export all operations to JSONL 343 + 344 + Input formats: 345 + - File path: reads DIDs from file 346 + - "-" or --stdin: reads DIDs from stdin 347 + - Omit file + use --stdin: reads from stdin`, 348 + 349 + Example: ` # Batch lookup from file 350 + plcbundle did batch dids.txt --action lookup 351 + 352 + # Read from stdin 353 + cat dids.txt | plcbundle did batch --stdin --action lookup 354 + cat dids.txt | plcbundle did batch - --action resolve 355 + 356 + # Export operations for DIDs from stdin 357 + echo "did:plc:524tuhdhh3m7li5gycdn6boe" | plcbundle did batch - --action export 358 + 359 + # Pipe results 360 + plcbundle did batch dids.txt --action resolve -o resolved.jsonl 361 + 362 + # Parallel processing 363 + cat dids.txt | plcbundle did batch --stdin --action lookup --workers 8 364 + 365 + # Chain commands 366 + grep "did:plc:" some_file.txt | plcbundle did batch - --action export > ops.jsonl`, 367 + 368 + Args: cobra.MaximumNArgs(1), 369 + 370 + RunE: func(cmd *cobra.Command, args []string) error { 371 + var filename string 372 + 373 + // Determine input source 374 + if len(args) > 0 { 375 + filename = args[0] 376 + if filename == "-" { 377 + fromStdin = true 378 + } 379 + } else if !fromStdin { 380 + return fmt.Errorf("either provide filename or use --stdin flag\n" + 381 + "Examples:\n" + 382 + " plcbundle did batch dids.txt\n" + 383 + " plcbundle did batch --stdin\n" + 384 + " cat dids.txt | plcbundle did batch -") 385 + } 386 + 387 + mgr, _, err := getManagerFromCommand(cmd, "") 388 + if err != nil { 389 + return err 390 + } 391 + defer mgr.Close() 392 + 393 + return processBatchDIDs(mgr, filename, batchOptions{ 394 + action: action, 395 + workers: workers, 396 + outputFile: outputFile, 397 + fromStdin: fromStdin, 398 + }) 399 + }, 400 + } 401 + 402 + cmd.Flags().StringVar(&action, "action", "lookup", "Action: lookup, resolve, export") 403 + cmd.Flags().IntVar(&workers, "workers", 4, "Number of parallel workers") 404 + cmd.Flags().StringVarP(&outputFile, "output", "o", "", "Output file (default: stdout)") 405 + cmd.Flags().BoolVar(&fromStdin, "stdin", false, "Read DIDs from stdin") 406 + 407 + return cmd 408 + } 409 + 410 + // ============================================================================ 411 + // DID STATS - Show DID activity statistics 412 + // ============================================================================ 413 + 414 + func newDIDStatsCommand() *cobra.Command { 415 + var ( 416 + showGlobal bool 417 + showJSON bool 418 + ) 419 + 420 + cmd := &cobra.Command{ 421 + Use: "stats [did]", 422 + Short: "Show DID activity statistics", 423 + Long: `Show DID activity statistics 424 + 425 + Display statistics for a specific DID or global DID index stats. 426 + 427 + With DID: shows operation count, first/last activity, bundle distribution 428 + Without DID: shows global index statistics`, 429 + 430 + Example: ` # Stats for specific DID 431 + plcbundle did stats did:plc:524tuhdhh3m7li5gycdn6boe 432 + 433 + # Global index stats 434 + plcbundle did stats --global 435 + plcbundle did stats 436 + 437 + # JSON output 438 + plcbundle did stats did:plc:524tuhdhh3m7li5gycdn6boe --json`, 439 + 440 + Args: cobra.MaximumNArgs(1), 441 + 442 + RunE: func(cmd *cobra.Command, args []string) error { 443 + mgr, dir, err := getManagerFromCommand(cmd, "") 444 + if err != nil { 445 + return err 446 + } 447 + defer mgr.Close() 448 + 449 + // Global stats 450 + if len(args) == 0 || showGlobal { 451 + return showGlobalDIDStats(mgr, dir, showJSON) 452 + } 453 + 454 + // Specific DID stats 455 + did := args[0] 456 + return showDIDStats(mgr, did, showJSON) 457 + }, 458 + } 459 + 460 + cmd.Flags().BoolVar(&showGlobal, "global", false, "Show global index stats") 461 + cmd.Flags().BoolVar(&showJSON, "json", false, "Output as JSON") 462 + 463 + return cmd 464 + } 465 + 466 + // ============================================================================ 467 + // Helper Functions 468 + // ============================================================================ 469 + 470 + type batchOptions struct { 471 + action string 472 + workers int 473 + outputFile string 474 + fromStdin bool 475 + } 476 + 477 + func processBatchDIDs(mgr BundleManager, filename string, opts batchOptions) error { 478 + // Determine input source 479 + var input *os.File 480 + var err error 481 + 482 + if opts.fromStdin { 483 + input = os.Stdin 484 + fmt.Fprintf(os.Stderr, "Reading DIDs from stdin...\n") 485 + } else { 486 + input, err = os.Open(filename) 487 + if err != nil { 488 + return fmt.Errorf("failed to open file: %w", err) 489 + } 490 + defer input.Close() 491 + fmt.Fprintf(os.Stderr, "Reading DIDs from: %s\n", filename) 492 + } 493 + 494 + // Read DIDs 495 + var dids []string 496 + scanner := bufio.NewScanner(input) 497 + 498 + // Increase buffer size for large input 499 + buf := make([]byte, 64*1024) 500 + scanner.Buffer(buf, 1024*1024) 501 + 502 + lineNum := 0 503 + for scanner.Scan() { 504 + lineNum++ 505 + line := strings.TrimSpace(scanner.Text()) 506 + 507 + // Skip empty lines and comments 508 + if line == "" || strings.HasPrefix(line, "#") { 509 + continue 510 + } 511 + 512 + // Basic validation 513 + if !strings.HasPrefix(line, "did:plc:") { 514 + fmt.Fprintf(os.Stderr, "⚠️ Line %d: skipping invalid DID: %s\n", lineNum, line) 515 + continue 516 + } 517 + 518 + dids = append(dids, line) 519 + } 520 + 521 + if err := scanner.Err(); err != nil { 522 + return fmt.Errorf("error reading input: %w", err) 523 + } 524 + 525 + if len(dids) == 0 { 526 + return fmt.Errorf("no valid DIDs found in input") 527 + } 528 + 529 + fmt.Fprintf(os.Stderr, "Processing %d DIDs with action '%s' (%d workers)\n\n", 530 + len(dids), opts.action, opts.workers) 531 + 532 + // Setup output 533 + var output *os.File 534 + if opts.outputFile != "" { 535 + output, err = os.Create(opts.outputFile) 536 + if err != nil { 537 + return fmt.Errorf("failed to create output file: %w", err) 538 + } 539 + defer output.Close() 540 + fmt.Fprintf(os.Stderr, "Output: %s\n\n", opts.outputFile) 541 + } else { 542 + output = os.Stdout 543 + } 544 + 545 + // Process based on action 546 + switch opts.action { 547 + case "lookup": 548 + return batchLookup(mgr, dids, output, opts.workers) 549 + case "resolve": 550 + return batchResolve(mgr, dids, output, opts.workers) 551 + case "export": 552 + return batchExport(mgr, dids, output, opts.workers) 553 + default: 554 + return fmt.Errorf("unknown action: %s (valid: lookup, resolve, export)", opts.action) 555 + } 556 + } 557 + 558 + func showGlobalDIDStats(mgr BundleManager, dir string, showJSON bool) error { 559 + stats := mgr.GetDIDIndexStats() 560 + 561 + if !stats["exists"].(bool) { 562 + fmt.Printf("DID index does not exist\n") 563 + fmt.Printf("Run: plcbundle index build\n") 564 + return nil 565 + } 566 + 567 + if showJSON { 568 + data, _ := json.MarshalIndent(stats, "", " ") 569 + fmt.Println(string(data)) 570 + return nil 571 + } 572 + 573 + indexedDIDs := stats["indexed_dids"].(int64) 574 + mempoolDIDs := stats["mempool_dids"].(int64) 575 + totalDIDs := stats["total_dids"].(int64) 576 + 577 + fmt.Printf("\nDID Index Statistics\n") 578 + fmt.Printf("════════════════════\n\n") 579 + fmt.Printf(" Location: %s/.plcbundle/\n", dir) 580 + 581 + if mempoolDIDs > 0 { 582 + fmt.Printf(" Indexed DIDs: %s (in bundles)\n", formatNumber(int(indexedDIDs))) 583 + fmt.Printf(" Mempool DIDs: %s (not yet bundled)\n", formatNumber(int(mempoolDIDs))) 584 + fmt.Printf(" Total DIDs: %s\n", formatNumber(int(totalDIDs))) 585 + } else { 586 + fmt.Printf(" Total DIDs: %s\n", formatNumber(int(totalDIDs))) 587 + } 588 + 589 + fmt.Printf(" Shard count: %d\n", stats["shard_count"]) 590 + fmt.Printf(" Last bundle: %06d\n", stats["last_bundle"]) 591 + fmt.Printf(" Updated: %s\n\n", stats["updated_at"].(time.Time).Format("2006-01-02 15:04:05")) 592 + 593 + fmt.Printf(" Cached shards: %d / %d\n", stats["cached_shards"], stats["cache_limit"]) 594 + 595 + if cachedList, ok := stats["cache_order"].([]int); ok && len(cachedList) > 0 { 596 + fmt.Printf(" Hot shards: ") 597 + for i, shard := range cachedList { 598 + if i > 0 { 599 + fmt.Printf(", ") 600 + } 601 + if i >= 10 { 602 + fmt.Printf("... (+%d more)", len(cachedList)-10) 603 + break 604 + } 605 + fmt.Printf("%02x", shard) 606 + } 607 + fmt.Printf("\n") 608 + } 609 + 610 + fmt.Printf("\n") 611 + return nil 612 + } 613 + 614 + func showDIDStats(mgr BundleManager, did string, showJSON bool) error { 615 + ctx := context.Background() 616 + 617 + // Get operations 618 + opsWithLoc, err := mgr.GetDIDOperationsWithLocations(ctx, did, false) 619 + if err != nil { 620 + return err 621 + } 622 + 623 + mempoolOps, err := mgr.GetDIDOperationsFromMempool(did) 624 + if err != nil { 625 + return err 626 + } 627 + 628 + if len(opsWithLoc) == 0 && len(mempoolOps) == 0 { 629 + fmt.Fprintf(os.Stderr, "DID not found: %s\n", did) 630 + return nil 631 + } 632 + 633 + // Calculate stats 634 + totalOps := len(opsWithLoc) + len(mempoolOps) 635 + nullifiedCount := 0 636 + for _, owl := range opsWithLoc { 637 + if owl.Operation.IsNullified() { 638 + nullifiedCount++ 639 + } 640 + } 641 + 642 + bundleSpan := 0 643 + if len(opsWithLoc) > 0 { 644 + bundles := make(map[int]bool) 645 + for _, owl := range opsWithLoc { 646 + bundles[owl.Bundle] = true 647 + } 648 + bundleSpan = len(bundles) 649 + } 650 + 651 + if showJSON { 652 + output := map[string]interface{}{ 653 + "did": did, 654 + "total_operations": totalOps, 655 + "bundled": len(opsWithLoc), 656 + "mempool": len(mempoolOps), 657 + "nullified": nullifiedCount, 658 + "active": totalOps - nullifiedCount, 659 + "bundle_span": bundleSpan, 660 + } 661 + data, _ := json.MarshalIndent(output, "", " ") 662 + fmt.Println(string(data)) 663 + return nil 664 + } 665 + 666 + fmt.Printf("\nDID Statistics\n") 667 + fmt.Printf("══════════════\n\n") 668 + fmt.Printf(" DID: %s\n\n", did) 669 + fmt.Printf(" Total operations: %d\n", totalOps) 670 + fmt.Printf(" Active: %d\n", totalOps-nullifiedCount) 671 + if nullifiedCount > 0 { 672 + fmt.Printf(" Nullified: %d\n", nullifiedCount) 673 + } 674 + if len(opsWithLoc) > 0 { 675 + fmt.Printf(" Bundled: %d\n", len(opsWithLoc)) 676 + fmt.Printf(" Bundle span: %d bundles\n", bundleSpan) 677 + } 678 + if len(mempoolOps) > 0 { 679 + fmt.Printf(" Mempool: %d\n", len(mempoolOps)) 680 + } 681 + fmt.Printf("\n") 682 + 683 + return nil 684 + } 685 + 686 + func displayHistory(did string, opsWithLoc []PLCOperationWithLocation, mempoolOps []plcclient.PLCOperation, compact bool, includeNullified bool) error { 687 + if compact { 688 + return displayHistoryCompact(did, opsWithLoc, mempoolOps, includeNullified) 689 + } 690 + return displayHistoryDetailed(did, opsWithLoc, mempoolOps, includeNullified) 691 + } 692 + 693 + func displayHistoryCompact(did string, opsWithLoc []PLCOperationWithLocation, mempoolOps []plcclient.PLCOperation, includeNullified bool) error { 694 + fmt.Printf("DID History: %s\n\n", did) 695 + 696 + for _, owl := range opsWithLoc { 697 + if !includeNullified && owl.Operation.IsNullified() { 698 + continue 699 + } 700 + 701 + status := "✓" 702 + if owl.Operation.IsNullified() { 703 + status = "✗" 704 + } 705 + 706 + fmt.Printf("%s [%06d:%04d] %s %s\n", 707 + status, 708 + owl.Bundle, 709 + owl.Position, 710 + owl.Operation.CreatedAt.Format("2006-01-02 15:04:05"), 711 + owl.Operation.CID) 712 + } 713 + 714 + for _, op := range mempoolOps { 715 + fmt.Printf("✓ [mempool ] %s %s\n", 716 + op.CreatedAt.Format("2006-01-02 15:04:05"), 717 + op.CID) 718 + } 719 + 720 + return nil 721 + } 722 + 723 + func displayHistoryDetailed(did string, opsWithLoc []PLCOperationWithLocation, mempoolOps []plcclient.PLCOperation, includeNullified bool) error { 724 + fmt.Printf("═══════════════════════════════════════════════════════════════\n") 725 + fmt.Printf(" DID Audit Log\n") 726 + fmt.Printf("═══════════════════════════════════════════════════════════════\n\n") 727 + fmt.Printf("DID: %s\n\n", did) 728 + 729 + for i, owl := range opsWithLoc { 730 + if !includeNullified && owl.Operation.IsNullified() { 731 + continue 732 + } 733 + 734 + op := owl.Operation 735 + status := "✓ Active" 736 + if op.IsNullified() { 737 + status = "✗ Nullified" 738 + } 739 + 740 + fmt.Printf("Operation %d [Bundle %06d, Position %04d]\n", i+1, owl.Bundle, owl.Position) 741 + fmt.Printf(" CID: %s\n", op.CID) 742 + fmt.Printf(" Created: %s\n", op.CreatedAt.Format("2006-01-02 15:04:05.000 MST")) 743 + fmt.Printf(" Status: %s\n", status) 744 + 745 + if opData, err := op.GetOperationData(); err == nil && opData != nil { 746 + showOperationDetails(&op) 747 + } 748 + 749 + fmt.Printf("\n") 750 + } 751 + 752 + if len(mempoolOps) > 0 { 753 + fmt.Printf("Mempool Operations (%d)\n", len(mempoolOps)) 754 + fmt.Printf("══════════════════════════════════════════════════════════════\n\n") 755 + 756 + for i, op := range mempoolOps { 757 + fmt.Printf("Operation %d [Mempool]\n", i+1) 758 + fmt.Printf(" CID: %s\n", op.CID) 759 + fmt.Printf(" Created: %s\n", op.CreatedAt.Format("2006-01-02 15:04:05.000 MST")) 760 + fmt.Printf(" Status: ✓ Active\n") 761 + fmt.Printf("\n") 762 + } 763 + } 764 + 765 + return nil 766 + } 767 + 768 + func outputHistoryJSON(did string, opsWithLoc []PLCOperationWithLocation, mempoolOps []plcclient.PLCOperation) error { 769 + output := map[string]interface{}{ 770 + "did": did, 771 + "bundled": make([]map[string]interface{}, 0), 772 + "mempool": make([]map[string]interface{}, 0), 773 + } 774 + 775 + for _, owl := range opsWithLoc { 776 + output["bundled"] = append(output["bundled"].([]map[string]interface{}), map[string]interface{}{ 777 + "bundle": owl.Bundle, 778 + "position": owl.Position, 779 + "cid": owl.Operation.CID, 780 + "nullified": owl.Operation.IsNullified(), 781 + "created_at": owl.Operation.CreatedAt.Format(time.RFC3339Nano), 782 + }) 783 + } 784 + 785 + for _, op := range mempoolOps { 786 + output["mempool"] = append(output["mempool"].([]map[string]interface{}), map[string]interface{}{ 787 + "cid": op.CID, 788 + "nullified": op.IsNullified(), 789 + "created_at": op.CreatedAt.Format(time.RFC3339Nano), 790 + }) 791 + } 792 + 793 + data, _ := json.MarshalIndent(output, "", " ") 794 + fmt.Println(string(data)) 795 + 796 + return nil 797 + } 798 + 799 + func batchLookup(mgr BundleManager, dids []string, output *os.File, workers int) error { 800 + progress := ui.NewProgressBar(len(dids)) 801 + ctx := context.Background() 802 + 803 + // CSV header 804 + fmt.Fprintf(output, "did,status,operation_count,bundled,mempool,nullified\n") 805 + 806 + found := 0 807 + notFound := 0 808 + errorCount := 0 809 + 810 + for i, did := range dids { 811 + opsWithLoc, err := mgr.GetDIDOperationsWithLocations(ctx, did, false) 812 + if err != nil { 813 + errorCount++ 814 + fmt.Fprintf(output, "%s,error,0,0,0,0\n", did) 815 + progress.Set(i + 1) 816 + continue 817 + } 818 + 819 + mempoolOps, _ := mgr.GetDIDOperationsFromMempool(did) 820 + 821 + if len(opsWithLoc) == 0 && len(mempoolOps) == 0 { 822 + notFound++ 823 + fmt.Fprintf(output, "%s,not_found,0,0,0,0\n", did) 824 + } else { 825 + found++ 826 + 827 + // Count nullified 828 + nullified := 0 829 + for _, owl := range opsWithLoc { 830 + if owl.Operation.IsNullified() { 831 + nullified++ 832 + } 833 + } 834 + 835 + fmt.Fprintf(output, "%s,found,%d,%d,%d,%d\n", 836 + did, 837 + len(opsWithLoc)+len(mempoolOps), 838 + len(opsWithLoc), 839 + len(mempoolOps), 840 + nullified) 841 + } 842 + 843 + progress.Set(i + 1) 844 + } 845 + 846 + progress.Finish() 847 + 848 + fmt.Fprintf(os.Stderr, "\n✓ Batch lookup complete\n") 849 + fmt.Fprintf(os.Stderr, " DIDs input: %d\n", len(dids)) 850 + fmt.Fprintf(os.Stderr, " Found: %d\n", found) 851 + fmt.Fprintf(os.Stderr, " Not found: %d\n", notFound) 852 + if errorCount > 0 { 853 + fmt.Fprintf(os.Stderr, " Errors: %d\n", errorCount) 854 + } 855 + 856 + return nil 857 + } 858 + 859 + func batchResolve(mgr BundleManager, dids []string, output *os.File, workers int) error { 860 + progress := ui.NewProgressBar(len(dids)) 861 + ctx := context.Background() 862 + 863 + resolved := 0 864 + failed := 0 865 + 866 + // Use buffered writer 867 + writer := bufio.NewWriterSize(output, 512*1024) 868 + defer writer.Flush() 869 + 870 + for i, did := range dids { 871 + result, err := mgr.ResolveDID(ctx, did) 872 + if err != nil { 873 + failed++ 874 + if i < 10 { 875 + fmt.Fprintf(os.Stderr, "Failed to resolve %s: %v\n", did, err) 876 + } 877 + } else { 878 + resolved++ 879 + data, _ := json.Marshal(result.Document) 880 + writer.Write(data) 881 + writer.WriteByte('\n') 882 + 883 + if i%100 == 0 { 884 + writer.Flush() 885 + } 886 + } 887 + 888 + progress.Set(i + 1) 889 + } 890 + 891 + writer.Flush() 892 + progress.Finish() 893 + 894 + fmt.Fprintf(os.Stderr, "\n✓ Batch resolve complete\n") 895 + fmt.Fprintf(os.Stderr, " DIDs input: %d\n", len(dids)) 896 + fmt.Fprintf(os.Stderr, " Resolved: %d\n", resolved) 897 + if failed > 0 { 898 + fmt.Fprintf(os.Stderr, " Failed: %d\n", failed) 899 + } 900 + 901 + return nil 902 + } 903 + 904 + func batchExport(mgr BundleManager, dids []string, output *os.File, workers int) error { 905 + progress := ui.NewProgressBar(len(dids)) 906 + ctx := context.Background() 907 + 908 + totalOps := 0 909 + processedDIDs := 0 910 + errorCount := 0 911 + 912 + // Use buffered writer for better performance 913 + writer := bufio.NewWriterSize(output, 512*1024) 914 + defer writer.Flush() 915 + 916 + for i, did := range dids { 917 + opsWithLoc, err := mgr.GetDIDOperationsWithLocations(ctx, did, false) 918 + if err != nil { 919 + errorCount++ 920 + if i < 10 { // Only log first few errors 921 + fmt.Fprintf(os.Stderr, "Error processing %s: %v\n", did, err) 922 + } 923 + progress.Set(i + 1) 924 + continue 925 + } 926 + 927 + // Get mempool operations too 928 + mempoolOps, _ := mgr.GetDIDOperationsFromMempool(did) 929 + 930 + if len(opsWithLoc) == 0 && len(mempoolOps) == 0 { 931 + progress.Set(i + 1) 932 + continue 933 + } 934 + 935 + processedDIDs++ 936 + 937 + // Export bundled operations 938 + for _, owl := range opsWithLoc { 939 + if len(owl.Operation.RawJSON) > 0 { 940 + writer.Write(owl.Operation.RawJSON) 941 + } else { 942 + data, _ := json.Marshal(owl.Operation) 943 + writer.Write(data) 944 + } 945 + writer.WriteByte('\n') 946 + totalOps++ 947 + } 948 + 949 + // Export mempool operations 950 + for _, op := range mempoolOps { 951 + if len(op.RawJSON) > 0 { 952 + writer.Write(op.RawJSON) 953 + } else { 954 + data, _ := json.Marshal(op) 955 + writer.Write(data) 956 + } 957 + writer.WriteByte('\n') 958 + totalOps++ 959 + } 960 + 961 + // Flush periodically 962 + if i%100 == 0 { 963 + writer.Flush() 964 + } 965 + 966 + progress.Set(i + 1) 967 + } 968 + 969 + writer.Flush() 970 + progress.Finish() 971 + 972 + fmt.Fprintf(os.Stderr, "\n✓ Batch export complete\n") 973 + fmt.Fprintf(os.Stderr, " DIDs input: %d\n", len(dids)) 974 + fmt.Fprintf(os.Stderr, " DIDs processed: %d\n", processedDIDs) 975 + fmt.Fprintf(os.Stderr, " Operations: %s\n", formatNumber(totalOps)) 976 + if errorCount > 0 { 977 + fmt.Fprintf(os.Stderr, " Errors: %d\n", errorCount) 978 + } 979 + 980 + return nil 981 + }
+3 -2
cmd/plcbundle/commands/stream.go
··· 21 21 ) 22 22 23 23 cmd := &cobra.Command{ 24 - Use: "stream [flags]", 25 - Short: "Stream operations to stdout (JSONL)", 24 + Use: "stream [flags]", 25 + Aliases: []string{"backfill"}, 26 + Short: "Stream operations to stdout (JSONL)", 26 27 Long: `Stream operations to stdout in JSONL format 27 28 28 29 Outputs PLC operations as newline-delimited JSON to stdout.
+2 -2
cmd/plcbundle/main.go
··· 60 60 cmd.AddCommand(commands.NewVerifyCommand()) 61 61 cmd.AddCommand(commands.NewDiffCommand()) 62 62 /*cmd.AddCommand(commands.NewStatsCommand()) 63 - cmd.AddCommand(commands.NewInspectCommand()) 63 + cmd.AddCommand(commands.NewInspectCommand())*/ 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
+22 -6
cmd/plcbundle/ui/progress.go
··· 102 102 103 103 remaining := pb.total - pb.current 104 104 var eta time.Duration 105 - if speed > 0 { 105 + if speed > 0 && remaining > 0 { 106 106 eta = time.Duration(float64(remaining)/speed) * time.Second 107 107 } 108 108 109 + // ✨ FIX: Check if complete 110 + isComplete := pb.current >= pb.total 111 + 109 112 if pb.showBytes && pb.currentBytes > 0 { 110 113 mbProcessed := float64(pb.currentBytes) / (1000 * 1000) 111 114 mbPerSec := mbProcessed / elapsed.Seconds() 112 115 113 - fmt.Fprintf(os.Stderr, "\r [%s] %6.2f%% | %d/%d | %.1f/s | %.1f MB/s | ETA: %s ", 114 - bar, percent, pb.current, pb.total, speed, mbPerSec, formatETA(eta)) 116 + if isComplete { 117 + // ✨ Don't show ETA when done 118 + fmt.Fprintf(os.Stderr, "\r [%s] %6.2f%% | %d/%d | %.1f/s | %.1f MB/s | Done ", 119 + bar, percent, pb.current, pb.total, speed, mbPerSec) 120 + } else { 121 + fmt.Fprintf(os.Stderr, "\r [%s] %6.2f%% | %d/%d | %.1f/s | %.1f MB/s | ETA: %s ", 122 + bar, percent, pb.current, pb.total, speed, mbPerSec, formatETA(eta)) 123 + } 115 124 } else { 116 - fmt.Fprintf(os.Stderr, "\r [%s] %6.2f%% | %d/%d | %.1f/s | ETA: %s ", 117 - bar, percent, pb.current, pb.total, speed, formatETA(eta)) 125 + if isComplete { 126 + // ✨ Don't show ETA when done 127 + fmt.Fprintf(os.Stderr, "\r [%s] %6.2f%% | %d/%d | %.1f/s | Done ", 128 + bar, percent, pb.current, pb.total, speed) 129 + } else { 130 + fmt.Fprintf(os.Stderr, "\r [%s] %6.2f%% | %d/%d | %.1f/s | ETA: %s ", 131 + bar, percent, pb.current, pb.total, speed, formatETA(eta)) 132 + } 118 133 } 119 134 } 120 135 121 136 func formatETA(d time.Duration) string { 137 + // ✨ This should never be called with 0 now, but keep as fallback 122 138 if d == 0 { 123 - return "calculating..." 139 + return "0s" 124 140 } 125 141 if d < time.Minute { 126 142 return fmt.Sprintf("%ds", int(d.Seconds()))