+981
cmd/plcbundle/commands/did.go
+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
+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
+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
+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()))