···327327328328 return stats
329329}
330330+331331+// GetDIDOperationsWithLocations returns operations along with their bundle/position info
332332+func (m *Manager) GetDIDOperationsWithLocations(ctx context.Context, did string, verbose bool) ([]PLCOperationWithLocation, error) {
333333+ if err := plc.ValidateDIDFormat(did); err != nil {
334334+ return nil, err
335335+ }
336336+337337+ // Set verbose mode
338338+ if m.didIndex != nil {
339339+ m.didIndex.SetVerbose(verbose)
340340+ }
341341+342342+ // Use index if available
343343+ if m.didIndex != nil && m.didIndex.Exists() {
344344+ if verbose {
345345+ m.logger.Printf("DEBUG: Using DID index for lookup with locations")
346346+ }
347347+ return m.getDIDOperationsWithLocationsIndexed(ctx, did, verbose)
348348+ }
349349+350350+ // Fallback to scan (slower, but still works)
351351+ if verbose {
352352+ m.logger.Printf("DEBUG: Using full scan with locations")
353353+ }
354354+ return m.getDIDOperationsWithLocationsScan(ctx, did)
355355+}
356356+357357+// getDIDOperationsWithLocationsIndexed uses index for fast lookup with locations
358358+func (m *Manager) getDIDOperationsWithLocationsIndexed(ctx context.Context, did string, verbose bool) ([]PLCOperationWithLocation, error) {
359359+ locations, err := m.didIndex.GetDIDLocations(did)
360360+ if err != nil {
361361+ return nil, err
362362+ }
363363+364364+ if len(locations) == 0 {
365365+ return []PLCOperationWithLocation{}, nil
366366+ }
367367+368368+ if verbose {
369369+ m.logger.Printf("DEBUG: Found %d locations in index", len(locations))
370370+ }
371371+372372+ // Load operations with their locations preserved
373373+ var results []PLCOperationWithLocation
374374+375375+ // Group by bundle for efficient loading
376376+ bundleMap := make(map[uint16][]OpLocation)
377377+ for _, loc := range locations {
378378+ bundleMap[loc.Bundle] = append(bundleMap[loc.Bundle], loc)
379379+ }
380380+381381+ if verbose {
382382+ m.logger.Printf("DEBUG: Loading from %d bundle(s)", len(bundleMap))
383383+ }
384384+385385+ for bundleNum, locs := range bundleMap {
386386+ bundle, err := m.LoadBundle(ctx, int(bundleNum))
387387+ if err != nil {
388388+ m.logger.Printf("Warning: failed to load bundle %d: %v", bundleNum, err)
389389+ continue
390390+ }
391391+392392+ for _, loc := range locs {
393393+ if int(loc.Position) >= len(bundle.Operations) {
394394+ continue
395395+ }
396396+397397+ op := bundle.Operations[loc.Position]
398398+ results = append(results, PLCOperationWithLocation{
399399+ Operation: op,
400400+ Bundle: int(loc.Bundle),
401401+ Position: int(loc.Position),
402402+ })
403403+ }
404404+ }
405405+406406+ // Sort by time
407407+ sort.Slice(results, func(i, j int) bool {
408408+ return results[i].Operation.CreatedAt.Before(results[j].Operation.CreatedAt)
409409+ })
410410+411411+ if verbose {
412412+ m.logger.Printf("DEBUG: Loaded %d total operations", len(results))
413413+ }
414414+415415+ return results, nil
416416+}
417417+418418+// getDIDOperationsWithLocationsScan falls back to full scan with locations
419419+func (m *Manager) getDIDOperationsWithLocationsScan(ctx context.Context, did string) ([]PLCOperationWithLocation, error) {
420420+ var results []PLCOperationWithLocation
421421+ bundles := m.index.GetBundles()
422422+423423+ for _, meta := range bundles {
424424+ select {
425425+ case <-ctx.Done():
426426+ return nil, ctx.Err()
427427+ default:
428428+ }
429429+430430+ bundle, err := m.LoadBundle(ctx, meta.BundleNumber)
431431+ if err != nil {
432432+ m.logger.Printf("Warning: failed to load bundle %d: %v", meta.BundleNumber, err)
433433+ continue
434434+ }
435435+436436+ for pos, op := range bundle.Operations {
437437+ if op.DID == did {
438438+ results = append(results, PLCOperationWithLocation{
439439+ Operation: op,
440440+ Bundle: meta.BundleNumber,
441441+ Position: pos,
442442+ })
443443+ }
444444+ }
445445+ }
446446+447447+ sort.Slice(results, func(i, j int) bool {
448448+ return results[i].Operation.CreatedAt.Before(results[j].Operation.CreatedAt)
449449+ })
450450+451451+ return results, nil
452452+}
+7
bundle/types.go
···213213 Interrupted bool
214214 FailedBundles []int
215215}
216216+217217+// PLCOperationWithLocation contains an operation with its bundle/position metadata
218218+type PLCOperationWithLocation struct {
219219+ Operation plc.PLCOperation
220220+ Bundle int
221221+ Position int
222222+}
+221-39
cmd/plcbundle/did_index.go
···55 "flag"
66 "fmt"
77 "os"
88+ "strings"
89 "time"
9101011 "github.com/goccy/go-json"
···176177177178 fs := flag.NewFlagSet("index lookup", flag.ExitOnError)
178179 verbose := fs.Bool("v", false, "verbose debug output")
180180+ showJSON := fs.Bool("json", false, "output as JSON")
179181 fs.Parse(os.Args[3:])
180182181183 if fs.NArg() < 1 {
182182- fmt.Fprintf(os.Stderr, "Usage: plcbundle index lookup <did> [-v]\n")
184184+ fmt.Fprintf(os.Stderr, "Usage: plcbundle index lookup <did> [-v] [--json]\n")
183185 os.Exit(1)
184186 }
185187···198200 fmt.Fprintf(os.Stderr, " Falling back to full scan (this will be slow)...\n\n")
199201 }
200202201201- fmt.Printf("Looking up: %s\n", did)
202202- if *verbose {
203203- fmt.Printf("Verbose mode: enabled\n")
203203+ if !*showJSON {
204204+ fmt.Printf("Looking up: %s\n", did)
205205+ if *verbose {
206206+ fmt.Printf("Verbose mode: enabled\n")
207207+ }
208208+ fmt.Printf("\n")
204209 }
205205- fmt.Printf("\n")
206210207207- start := time.Now()
211211+ // === TIMING START ===
212212+ totalStart := time.Now()
208213 ctx := context.Background()
209214210210- // Get bundled operations only
211211- bundledOps, err := mgr.GetDIDOperationsBundledOnly(ctx, did, *verbose)
215215+ // === STEP 1: Index/Scan Lookup ===
216216+ lookupStart := time.Now()
217217+ opsWithLoc, err := mgr.GetDIDOperationsWithLocations(ctx, did, *verbose)
212218 if err != nil {
213219 fmt.Fprintf(os.Stderr, "Error: %v\n", err)
214220 os.Exit(1)
215221 }
222222+ lookupElapsed := time.Since(lookupStart)
216223217217- // Get mempool operations separately
224224+ // === STEP 2: Mempool Lookup ===
225225+ mempoolStart := time.Now()
218226 mempoolOps, err := mgr.GetDIDOperationsFromMempool(did)
219227 if err != nil {
220228 fmt.Fprintf(os.Stderr, "Error checking mempool: %v\n", err)
221229 os.Exit(1)
222230 }
231231+ mempoolElapsed := time.Since(mempoolStart)
223232224224- elapsed := time.Since(start)
233233+ totalElapsed := time.Since(totalStart)
234234+235235+ // === NOT FOUND ===
236236+ if len(opsWithLoc) == 0 && len(mempoolOps) == 0 {
237237+ if *showJSON {
238238+ fmt.Println("{\"found\": false, \"operations\": []}")
239239+ } else {
240240+ fmt.Printf("DID not found (searched in %s)\n", totalElapsed)
241241+ }
242242+ return
243243+ }
244244+245245+ // === JSON OUTPUT MODE ===
246246+ if *showJSON {
247247+ output := map[string]interface{}{
248248+ "found": true,
249249+ "did": did,
250250+ "timing": map[string]interface{}{
251251+ "total_ms": totalElapsed.Milliseconds(),
252252+ "lookup_ms": lookupElapsed.Milliseconds(),
253253+ "mempool_ms": mempoolElapsed.Milliseconds(),
254254+ },
255255+ "bundled": make([]map[string]interface{}, 0),
256256+ "mempool": make([]map[string]interface{}, 0),
257257+ }
258258+259259+ for _, owl := range opsWithLoc {
260260+ output["bundled"] = append(output["bundled"].([]map[string]interface{}), map[string]interface{}{
261261+ "bundle": owl.Bundle,
262262+ "position": owl.Position,
263263+ "cid": owl.Operation.CID,
264264+ "nullified": owl.Operation.IsNullified(),
265265+ "created_at": owl.Operation.CreatedAt.Format(time.RFC3339Nano),
266266+ })
267267+ }
268268+269269+ for _, op := range mempoolOps {
270270+ output["mempool"] = append(output["mempool"].([]map[string]interface{}), map[string]interface{}{
271271+ "cid": op.CID,
272272+ "nullified": op.IsNullified(),
273273+ "created_at": op.CreatedAt.Format(time.RFC3339Nano),
274274+ })
275275+ }
225276226226- if len(bundledOps) == 0 && len(mempoolOps) == 0 {
227227- fmt.Printf("DID not found (searched in %s)\n", elapsed)
277277+ data, _ := json.MarshalIndent(output, "", " ")
278278+ fmt.Println(string(data))
228279 return
229280 }
230281231231- // Count nullified operations
282282+ // === CALCULATE STATISTICS ===
232283 nullifiedCount := 0
233233- for _, op := range bundledOps {
234234- if op.IsNullified() {
284284+ for _, owl := range opsWithLoc {
285285+ if owl.Operation.IsNullified() {
235286 nullifiedCount++
236287 }
237288 }
238289239239- // Display summary
240240- totalOps := len(bundledOps) + len(mempoolOps)
241241- fmt.Printf("Found %d total operations in %s\n", totalOps, elapsed)
242242- if len(bundledOps) > 0 {
243243- fmt.Printf(" Bundled: %d (%d active, %d nullified)\n", len(bundledOps), len(bundledOps)-nullifiedCount, nullifiedCount)
290290+ totalOps := len(opsWithLoc) + len(mempoolOps)
291291+ activeOps := len(opsWithLoc) - nullifiedCount + len(mempoolOps)
292292+293293+ // === DISPLAY SUMMARY ===
294294+ fmt.Printf("═══════════════════════════════════════════════════════════════\n")
295295+ fmt.Printf(" DID Lookup Results\n")
296296+ fmt.Printf("═══════════════════════════════════════════════════════════════\n\n")
297297+298298+ fmt.Printf("DID: %s\n\n", did)
299299+300300+ fmt.Printf("Summary\n")
301301+ fmt.Printf("───────\n")
302302+ fmt.Printf(" Total operations: %d\n", totalOps)
303303+ fmt.Printf(" Active operations: %d\n", activeOps)
304304+ if nullifiedCount > 0 {
305305+ fmt.Printf(" Nullified: %d\n", nullifiedCount)
306306+ }
307307+ if len(opsWithLoc) > 0 {
308308+ fmt.Printf(" Bundled: %d\n", len(opsWithLoc))
244309 }
245310 if len(mempoolOps) > 0 {
246246- fmt.Printf(" Mempool: %d (not yet bundled)\n", len(mempoolOps))
311311+ fmt.Printf(" Mempool: %d\n", len(mempoolOps))
247312 }
248313 fmt.Printf("\n")
249314250250- // Show bundled operations
251251- if len(bundledOps) > 0 {
252252- fmt.Printf("Bundled operations:\n")
253253- for i, op := range bundledOps {
254254- status := "✓"
315315+ // === TIMING BREAKDOWN ===
316316+ fmt.Printf("Performance\n")
317317+ fmt.Printf("───────────\n")
318318+ fmt.Printf(" Index lookup: %s\n", lookupElapsed)
319319+ fmt.Printf(" Mempool check: %s\n", mempoolElapsed)
320320+ fmt.Printf(" Total time: %s\n", totalElapsed)
321321+322322+ if len(opsWithLoc) > 0 {
323323+ avgPerOp := lookupElapsed / time.Duration(len(opsWithLoc))
324324+ fmt.Printf(" Avg per operation: %s\n", avgPerOp)
325325+ }
326326+ fmt.Printf("\n")
327327+328328+ // === BUNDLED OPERATIONS ===
329329+ if len(opsWithLoc) > 0 {
330330+ fmt.Printf("Bundled Operations (%d total)\n", len(opsWithLoc))
331331+ fmt.Printf("══════════════════════════════════════════════════════════════\n\n")
332332+333333+ for i, owl := range opsWithLoc {
334334+ op := owl.Operation
335335+ status := "✓ Active"
336336+ statusSymbol := "✓"
255337 if op.IsNullified() {
256256- status = "✗"
338338+ status = "✗ Nullified"
339339+ statusSymbol = "✗"
257340 }
258341259259- fmt.Printf(" %s %d. CID: %s\n", status, i+1, op.CID)
260260- fmt.Printf(" Time: %s\n", op.CreatedAt.Format("2006-01-02 15:04:05"))
342342+ fmt.Printf("%s Operation %d [Bundle %06d, Position %04d]\n",
343343+ statusSymbol, i+1, owl.Bundle, owl.Position)
344344+ fmt.Printf(" CID: %s\n", op.CID)
345345+ fmt.Printf(" Created: %s\n", op.CreatedAt.Format("2006-01-02 15:04:05.000 MST"))
346346+ fmt.Printf(" Status: %s\n", status)
261347262348 if op.IsNullified() {
263349 if nullCID := op.GetNullifyingCID(); nullCID != "" {
264264- fmt.Printf(" Nullified by: %s\n", nullCID)
265265- } else {
266266- fmt.Printf(" Nullified: true\n")
350350+ fmt.Printf(" Nullified: %s\n", nullCID)
351351+ }
352352+ }
353353+354354+ // Show operation type if verbose
355355+ if *verbose {
356356+ if opData, err := op.GetOperationData(); err == nil && opData != nil {
357357+ if opType, ok := opData["type"].(string); ok {
358358+ fmt.Printf(" Type: %s\n", opType)
359359+ }
360360+361361+ // Show handle if present
362362+ if handle, ok := opData["handle"].(string); ok {
363363+ fmt.Printf(" Handle: %s\n", handle)
364364+ } else if aka, ok := opData["alsoKnownAs"].([]interface{}); ok && len(aka) > 0 {
365365+ if akaStr, ok := aka[0].(string); ok {
366366+ handle := strings.TrimPrefix(akaStr, "at://")
367367+ fmt.Printf(" Handle: %s\n", handle)
368368+ }
369369+ }
370370+371371+ // Show service if present
372372+ if services, ok := opData["services"].(map[string]interface{}); ok {
373373+ if pds, ok := services["atproto_pds"].(map[string]interface{}); ok {
374374+ if endpoint, ok := pds["endpoint"].(string); ok {
375375+ fmt.Printf(" PDS: %s\n", endpoint)
376376+ }
377377+ }
378378+ }
267379 }
268380 }
381381+382382+ fmt.Printf("\n")
269383 }
270270- fmt.Printf("\n")
271384 }
272385273273- // Show mempool operations
386386+ // === MEMPOOL OPERATIONS ===
274387 if len(mempoolOps) > 0 {
275275- fmt.Printf("Mempool operations (not yet bundled):\n")
388388+ fmt.Printf("Mempool Operations (%d total, not yet bundled)\n", len(mempoolOps))
389389+ fmt.Printf("══════════════════════════════════════════════════════════════\n\n")
390390+276391 for i, op := range mempoolOps {
277277- status := "✓"
392392+ status := "✓ Active"
393393+ statusSymbol := "✓"
278394 if op.IsNullified() {
279279- status = "✗"
395395+ status = "✗ Nullified"
396396+ statusSymbol = "✗"
280397 }
281398282282- fmt.Printf(" %s %d. CID: %s\n", status, i+1, op.CID)
283283- fmt.Printf(" Time: %s\n", op.CreatedAt.Format("2006-01-02 15:04:05"))
399399+ fmt.Printf("%s Operation %d [Mempool]\n", statusSymbol, i+1)
400400+ fmt.Printf(" CID: %s\n", op.CID)
401401+ fmt.Printf(" Created: %s\n", op.CreatedAt.Format("2006-01-02 15:04:05.000 MST"))
402402+ fmt.Printf(" Status: %s\n", status)
403403+404404+ if op.IsNullified() {
405405+ if nullCID := op.GetNullifyingCID(); nullCID != "" {
406406+ fmt.Printf(" Nullified: %s\n", nullCID)
407407+ }
408408+ }
409409+410410+ // Show operation type if verbose
411411+ if *verbose {
412412+ if opData, err := op.GetOperationData(); err == nil && opData != nil {
413413+ if opType, ok := opData["type"].(string); ok {
414414+ fmt.Printf(" Type: %s\n", opType)
415415+ }
416416+417417+ // Show handle
418418+ if handle, ok := opData["handle"].(string); ok {
419419+ fmt.Printf(" Handle: %s\n", handle)
420420+ } else if aka, ok := opData["alsoKnownAs"].([]interface{}); ok && len(aka) > 0 {
421421+ if akaStr, ok := aka[0].(string); ok {
422422+ handle := strings.TrimPrefix(akaStr, "at://")
423423+ fmt.Printf(" Handle: %s\n", handle)
424424+ }
425425+ }
426426+ }
427427+ }
428428+429429+ fmt.Printf("\n")
284430 }
285431 }
432432+433433+ // === TIMELINE (if multiple operations) ===
434434+ if totalOps > 1 && !*verbose {
435435+ fmt.Printf("Timeline\n")
436436+ fmt.Printf("────────\n")
437437+438438+ allTimes := make([]time.Time, 0, totalOps)
439439+ for _, owl := range opsWithLoc {
440440+ allTimes = append(allTimes, owl.Operation.CreatedAt)
441441+ }
442442+ for _, op := range mempoolOps {
443443+ allTimes = append(allTimes, op.CreatedAt)
444444+ }
445445+446446+ if len(allTimes) > 0 {
447447+ firstTime := allTimes[0]
448448+ lastTime := allTimes[len(allTimes)-1]
449449+ timespan := lastTime.Sub(firstTime)
450450+451451+ fmt.Printf(" First operation: %s\n", firstTime.Format("2006-01-02 15:04:05"))
452452+ fmt.Printf(" Latest operation: %s\n", lastTime.Format("2006-01-02 15:04:05"))
453453+ fmt.Printf(" Timespan: %s\n", formatDuration(timespan))
454454+ fmt.Printf(" Activity age: %s ago\n", formatDuration(time.Since(lastTime)))
455455+ }
456456+ fmt.Printf("\n")
457457+ }
458458+459459+ // === FINAL SUMMARY ===
460460+ fmt.Printf("═══════════════════════════════════════════════════════════════\n")
461461+ fmt.Printf("✓ Lookup complete in %s\n", totalElapsed)
462462+ if stats["exists"].(bool) {
463463+ fmt.Printf(" Method: DID index (fast)\n")
464464+ } else {
465465+ fmt.Printf(" Method: Full scan (slow)\n")
466466+ }
467467+ fmt.Printf("═══════════════════════════════════════════════════════════════\n")
286468}
287469288470func cmdDIDIndexResolve() {