diff --git a/.gitignore b/.gitignore index 9694f2706..2257e0e7e 100644 --- a/.gitignore +++ b/.gitignore @@ -65,4 +65,7 @@ e2e/.bin e2e/.env.e2e # IDE -.cursor \ No newline at end of file +.cursor + +# Module build output +modules/*/bin diff --git a/buf.lock b/buf.lock index a2f0110b3..6285fd8f2 100644 --- a/buf.lock +++ b/buf.lock @@ -5,5 +5,5 @@ deps: commit: 62f35d8aed1149c291d606d958a7ce32 digest: b5:d66bf04adc77a0870bdc9328aaf887c7188a36fb02b83a480dc45ef9dc031b4d39fc6e9dc6435120ccf4fe5bfd5c6cb6592533c6c316595571f9a31420ab47fe - name: buf.build/viamrobotics/api - commit: 17805817b2b2439aa3a600f5b2ccf3df - digest: b5:b40d821129b8a6a7561e1c455a2b8da8fb243da83970ad6566fc23a8652ff586f92d52c1ebb0811738a24bdf7e689c1ea5920e01e71448ba64fc1a9a2667addc + commit: 998407192f184de9a96c94adb8513244 + digest: b5:1a0ed5e0080de164d97a288a8626227ee4f4bb61d6748de99063852082c1f5573c7922fe8cd0aadbb3b1bfe1b41d9e82e2ce9adb732462cf097a050fef294927 diff --git a/draw/draw_service.go b/draw/draw_service.go index 081bf145a..abdc34fe1 100644 --- a/draw/draw_service.go +++ b/draw/draw_service.go @@ -318,6 +318,9 @@ func (svc *DrawService) addEntityLocked(msg *drawv1.AddEntityRequest) (uuid.UUID return id, connect.NewError(connect.CodeInvalidArgument, errors.New("transform is required")) } id = resolveEntityUUID(e.Transform.GetUuid()) + // Stamped before storing: a server-assigned UUID is only returned to the caller, so + // without this the stored entity and every stream event carry an empty one. + e.Transform.Uuid = id[:] _, exists := svc.entities[id] changeType := addedOrUpdated(exists) svc.entities[id] = storedEntity{kind: entityKindTransform, transform: e.Transform} @@ -330,6 +333,7 @@ func (svc *DrawService) addEntityLocked(msg *drawv1.AddEntityRequest) (uuid.UUID return id, connect.NewError(connect.CodeInvalidArgument, errors.New("drawing is required")) } id = resolveEntityUUID(e.Drawing.GetUuid()) + e.Drawing.Uuid = id[:] _, exists := svc.entities[id] changeType := addedOrUpdated(exists) svc.entities[id] = storedEntity{kind: entityKindDrawing, drawing: e.Drawing} @@ -1207,72 +1211,27 @@ func (svc *DrawService) StreamEntityChanges( _ *connect.Request[drawv1.StreamEntityChangesRequest], stream *connect.ServerStream[drawv1.StreamEntityChangesResponse], ) error { - svc.mu.Lock() - subID, sub := svc.addEntitySub() - - replay := make([]*drawv1.StreamEntityChangesResponse, 0, len(svc.entities)) - for id, entity := range svc.entities { - switch entity.kind { - case entityKindTransform: - replay = append(replay, &drawv1.StreamEntityChangesResponse{ - ChangeType: drawv1.EntityChangeType_ENTITY_CHANGE_TYPE_ADDED, - Entity: &drawv1.StreamEntityChangesResponse_Transform{Transform: entity.transform}, - }) - case entityKindDrawing: - if chunked, ok := svc.chunked[id]; ok { - if msg := svc.buildChunkedReplayMsg(chunked); msg != nil { - replay = append(replay, msg) - } - } else { - replay = append(replay, &drawv1.StreamEntityChangesResponse{ - ChangeType: drawv1.EntityChangeType_ENTITY_CHANGE_TYPE_ADDED, - Entity: &drawv1.StreamEntityChangesResponse_Drawing{Drawing: entity.drawing}, - }) - } - } - } - svc.mu.Unlock() - - // Registered before the replay loop: a client that disconnects mid-replay - // would otherwise leave its channel in svc.entitySubs with nobody draining - // it, filling up and logging a drop on every subsequent mutation forever. - defer func() { - svc.mu.Lock() - svc.removeEntitySub(subID) - svc.mu.Unlock() - }() - - for _, msg := range replay { - if err := stream.Send(msg); err != nil { - return err - } - } + // Registered before the replay is sent: a client that disconnects mid-replay would + // otherwise leave its channel in svc.entitySubs with nobody draining it, filling up and + // logging a drop on every subsequent mutation forever. + subscription := svc.SubscribeEntities() + defer subscription.Close() for { - msgs, overflow, closed := sub.take() - if overflow { - log.Printf("draw: subscriber %d fell more than %d changes behind; closing stream for resync", subID, maxPendingEntityChanges) - return connect.NewError( - connect.CodeResourceExhausted, - errors.New("entity change queue overflow; reconnect for a fresh snapshot"), - ) + msgs, err := subscription.Next(ctx) + switch { + case errors.Is(err, ErrSubscriberOverflow): + log.Printf("draw: subscriber %d fell more than %d changes behind; closing stream for resync", subscription.id, maxPendingEntityChanges) + return connect.NewError(connect.CodeResourceExhausted, ErrSubscriberOverflow) + case err != nil: + return nil } + for _, msg := range msgs { if err := stream.Send(msg); err != nil { return err } } - if len(msgs) > 0 { - continue - } - if closed { - return nil - } - select { - case <-ctx.Done(): - return nil - case <-sub.notify: - } } } diff --git a/draw/draw_service_test.go b/draw/draw_service_test.go index bf54f6973..cae75f5c4 100644 --- a/draw/draw_service_test.go +++ b/draw/draw_service_test.go @@ -128,6 +128,39 @@ func TestDrawService_AddEntity(t *testing.T) { test.That(t, resp.Msg.GetUuid(), test.ShouldHaveLength, 16) }) + t.Run("ServerAssignedUUIDReachesTheStream", func(t *testing.T) { + svc := NewDrawService(t.TempDir()) + client := newTestServer(t, svc) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + type streamResult struct { + stream *connect.ServerStreamForClient[drawv1.StreamEntityChangesResponse] + err error + } + sCh := make(chan streamResult, 1) + go func() { + s, err := client.StreamEntityChanges(ctx, connect.NewRequest(&drawv1.StreamEntityChangesRequest{})) + sCh <- streamResult{s, err} + }() + + waitForEntitySubs(t, svc, 1) + + // No UUID supplied, so the service mints one. A consumer keying off the stream can only + // track the entity if that UUID is stamped onto the broadcast entity too. + resp, err := client.AddEntity(context.Background(), connect.NewRequest(&drawv1.AddEntityRequest{ + Entity: &drawv1.AddEntityRequest_Drawing{Drawing: sampleDrawing("drawing-1")}, + })) + test.That(t, err, test.ShouldBeNil) + + sr := <-sCh + test.That(t, sr.err, test.ShouldBeNil) + + test.That(t, sr.stream.Receive(), test.ShouldBeTrue) + test.That(t, sr.stream.Msg().GetDrawing().GetUuid(), test.ShouldResemble, resp.Msg.GetUuid()) + }) + t.Run("AddMultipleEntitiesReturnsUniqueUUIDs", func(t *testing.T) { svc := NewDrawService(t.TempDir()) client := newTestServer(t, svc) diff --git a/draw/entity_subscription.go b/draw/entity_subscription.go new file mode 100644 index 000000000..501a920eb --- /dev/null +++ b/draw/entity_subscription.go @@ -0,0 +1,101 @@ +package draw + +import ( + "context" + "errors" + "io" + "sync" + + drawv1 "github.com/viam-labs/motion-tools/draw/v1" +) + +// ErrSubscriberOverflow reports that a subscription fell too far behind. The stream is +// incremental, so the backlog cannot be salvaged; resubscribe for a fresh replay. +var ErrSubscriberOverflow = errors.New("entity change queue overflow; reconnect for a fresh snapshot") + +// EntitySubscription is a transport-independent view of the entity change stream, so a consumer +// outside this package can follow the scene without going through Connect-RPC. +type EntitySubscription struct { + svc *DrawService + id uint64 + sub *entitySubscriber + replay []*drawv1.StreamEntityChangesResponse + + closeOnce sync.Once +} + +// SubscribeEntities registers a subscriber against the current world. Callers must Close it: an +// abandoned subscription queues changes nobody drains until it overflows. +func (svc *DrawService) SubscribeEntities() *EntitySubscription { + // One lock covers the snapshot and the registration, so no change can slip between them. + svc.mu.Lock() + defer svc.mu.Unlock() + + id, sub := svc.addEntitySub() + + replay := make([]*drawv1.StreamEntityChangesResponse, 0, len(svc.entities)) + for entityID, entity := range svc.entities { + switch entity.kind { + case entityKindTransform: + replay = append(replay, &drawv1.StreamEntityChangesResponse{ + ChangeType: drawv1.EntityChangeType_ENTITY_CHANGE_TYPE_ADDED, + Entity: &drawv1.StreamEntityChangesResponse_Transform{Transform: entity.transform}, + }) + case entityKindDrawing: + if chunked, ok := svc.chunked[entityID]; ok { + if msg := svc.buildChunkedReplayMsg(chunked); msg != nil { + replay = append(replay, msg) + } + } else { + replay = append(replay, &drawv1.StreamEntityChangesResponse{ + ChangeType: drawv1.EntityChangeType_ENTITY_CHANGE_TYPE_ADDED, + Entity: &drawv1.StreamEntityChangesResponse_Drawing{Drawing: entity.drawing}, + }) + } + } + } + + return &EntitySubscription{svc: svc, id: id, sub: sub, replay: replay} +} + +// Next blocks for the next batch of changes: the world replayed as ADDED events on the first +// call, live changes after. +// +// Returns io.EOF when closed or cancelled, ErrSubscriberOverflow when the consumer fell behind. +func (subscription *EntitySubscription) Next( + ctx context.Context, +) ([]*drawv1.StreamEntityChangesResponse, error) { + if len(subscription.replay) > 0 { + replay := subscription.replay + subscription.replay = nil + return replay, nil + } + + for { + msgs, overflow, closed := subscription.sub.take() + if overflow { + return nil, ErrSubscriberOverflow + } + if len(msgs) > 0 { + return msgs, nil + } + if closed { + return nil, io.EOF + } + + select { + case <-ctx.Done(): + return nil, io.EOF + case <-subscription.sub.notify: + } + } +} + +// Close unregisters the subscription. Safe to call more than once. +func (subscription *EntitySubscription) Close() { + subscription.closeOnce.Do(func() { + subscription.svc.mu.Lock() + subscription.svc.removeEntitySub(subscription.id) + subscription.svc.mu.Unlock() + }) +} diff --git a/draw/v1/drawing.pb.go b/draw/v1/drawing.pb.go index a0f24b22e..3192719f1 100644 --- a/draw/v1/drawing.pb.go +++ b/draw/v1/drawing.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.11 +// protoc-gen-go v1.36.12 // protoc (unknown) // source: draw/v1/drawing.proto diff --git a/draw/v1/metadata.pb.go b/draw/v1/metadata.pb.go index 5aad73b71..8cfe41607 100644 --- a/draw/v1/metadata.pb.go +++ b/draw/v1/metadata.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.11 +// protoc-gen-go v1.36.12 // protoc (unknown) // source: draw/v1/metadata.proto diff --git a/draw/v1/scene.pb.go b/draw/v1/scene.pb.go index 0124123cf..f86d9fb80 100644 --- a/draw/v1/scene.pb.go +++ b/draw/v1/scene.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.11 +// protoc-gen-go v1.36.12 // protoc (unknown) // source: draw/v1/scene.proto diff --git a/draw/v1/service.pb.go b/draw/v1/service.pb.go index 545ebc6b3..d0bf2f1a3 100644 --- a/draw/v1/service.pb.go +++ b/draw/v1/service.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.11 +// protoc-gen-go v1.36.12 // protoc (unknown) // source: draw/v1/service.proto diff --git a/draw/v1/snapshot.pb.go b/draw/v1/snapshot.pb.go index 8f86ffafe..598db2374 100644 --- a/draw/v1/snapshot.pb.go +++ b/draw/v1/snapshot.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.11 +// protoc-gen-go v1.36.12 // protoc (unknown) // source: draw/v1/snapshot.proto diff --git a/draw/v1/transforms.pb.go b/draw/v1/transforms.pb.go index 9a3cd65bf..aaf6b1e8d 100644 --- a/draw/v1/transforms.pb.go +++ b/draw/v1/transforms.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.11 +// protoc-gen-go v1.36.12 // protoc (unknown) // source: draw/v1/transforms.proto diff --git a/draw/v1/worldstate.pb.go b/draw/v1/worldstate.pb.go new file mode 100644 index 000000000..8e335d003 --- /dev/null +++ b/draw/v1/worldstate.pb.go @@ -0,0 +1,134 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.12 +// protoc (unknown) +// source: draw/v1/worldstate.proto + +package drawv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// DrawingProjection is merged into a viam.common.v1.Transform's metadata when a Drawing +// travels over the world_state_store API. +// +// That API carries only common.v1.Geometry, so a Shape has no native home and rides in the +// metadata Struct instead, next to draw.v1.Metadata's fields. +// +// Both halves are encoded with protojson using proto field names and numeric enums. +type DrawingProjection struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The shape this transform really is. Present only on projected Drawings; a Transform + // that carries no shape is a true Transform and renders from its physical_object. + Shape *Shape `protobuf:"bytes,1,opt,name=shape,proto3" json:"shape,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DrawingProjection) Reset() { + *x = DrawingProjection{} + mi := &file_draw_v1_worldstate_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DrawingProjection) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DrawingProjection) ProtoMessage() {} + +func (x *DrawingProjection) ProtoReflect() protoreflect.Message { + mi := &file_draw_v1_worldstate_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DrawingProjection.ProtoReflect.Descriptor instead. +func (*DrawingProjection) Descriptor() ([]byte, []int) { + return file_draw_v1_worldstate_proto_rawDescGZIP(), []int{0} +} + +func (x *DrawingProjection) GetShape() *Shape { + if x != nil { + return x.Shape + } + return nil +} + +var File_draw_v1_worldstate_proto protoreflect.FileDescriptor + +const file_draw_v1_worldstate_proto_rawDesc = "" + + "\n" + + "\x18draw/v1/worldstate.proto\x12\adraw.v1\x1a\x15draw/v1/drawing.proto\"9\n" + + "\x11DrawingProjection\x12$\n" + + "\x05shape\x18\x01 \x01(\v2\x0e.draw.v1.ShapeR\x05shapeB2Z0github.com/viam-labs/motion-tools/draw/v1;drawv1b\x06proto3" + +var ( + file_draw_v1_worldstate_proto_rawDescOnce sync.Once + file_draw_v1_worldstate_proto_rawDescData []byte +) + +func file_draw_v1_worldstate_proto_rawDescGZIP() []byte { + file_draw_v1_worldstate_proto_rawDescOnce.Do(func() { + file_draw_v1_worldstate_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_draw_v1_worldstate_proto_rawDesc), len(file_draw_v1_worldstate_proto_rawDesc))) + }) + return file_draw_v1_worldstate_proto_rawDescData +} + +var file_draw_v1_worldstate_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_draw_v1_worldstate_proto_goTypes = []any{ + (*DrawingProjection)(nil), // 0: draw.v1.DrawingProjection + (*Shape)(nil), // 1: draw.v1.Shape +} +var file_draw_v1_worldstate_proto_depIdxs = []int32{ + 1, // 0: draw.v1.DrawingProjection.shape:type_name -> draw.v1.Shape + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_draw_v1_worldstate_proto_init() } +func file_draw_v1_worldstate_proto_init() { + if File_draw_v1_worldstate_proto != nil { + return + } + file_draw_v1_drawing_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_draw_v1_worldstate_proto_rawDesc), len(file_draw_v1_worldstate_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_draw_v1_worldstate_proto_goTypes, + DependencyIndexes: file_draw_v1_worldstate_proto_depIdxs, + MessageInfos: file_draw_v1_worldstate_proto_msgTypes, + }.Build() + File_draw_v1_worldstate_proto = out.File + file_draw_v1_worldstate_proto_goTypes = nil + file_draw_v1_worldstate_proto_depIdxs = nil +} diff --git a/e2e/setup.sh b/e2e/setup.sh index 991a90f18..22ea74e8e 100755 --- a/e2e/setup.sh +++ b/e2e/setup.sh @@ -58,6 +58,20 @@ fi echo "" +echo "--- Building visualization module ---" +VIZ_MODULE_DIR=$SCRIPT_DIR/../modules/visualization +VIZ_MODULE_BIN=$BIN_DIR/visualization + +if [ -d "$VIZ_MODULE_DIR" ]; then + (cd "$VIZ_MODULE_DIR" && go build -o "$VIZ_MODULE_BIN" .) + echo "Built visualization module to $VIZ_MODULE_BIN" +else + echo "visualization module directory not found at $VIZ_MODULE_DIR." + exit 1 +fi + +echo "" + echo "--- Building docs obstacle-store example ---" OBS_MODULE_DIR=$SCRIPT_DIR/../docs/examples/worldstatestore OBS_MODULE_BIN=$BIN_DIR/obstacle-store diff --git a/e2e/visualization-module.test.ts b/e2e/visualization-module.test.ts new file mode 100644 index 000000000..09b1867a9 --- /dev/null +++ b/e2e/visualization-module.test.ts @@ -0,0 +1,178 @@ +import { expect } from '@playwright/test' +import { type JsonValue, Struct } from '@viamrobotics/sdk' +import { execSync } from 'node:child_process' +import path from 'node:path' +import url from 'node:url' + +import { connectViamClient, getE2EConfig, withRobot } from './fixtures/with-robot' + +const dirname = path.dirname(url.fileURLToPath(import.meta.url)) +const moduleDir = path.resolve(dirname, '../modules/visualization') + +/** + * Drives one subtest of the module's Go e2e suite, which writes to the machine over DoCommand. + * Each call changes exactly one thing, so the screenshot taken after it has a single cause. + */ +const draw = (testPath: string) => { + try { + const output = execSync(`go test -run '${testPath}' . -count=1 -v -timeout=120s 2>&1`, { + encoding: 'utf8', + cwd: moduleDir, + }) + console.log(output) + } catch (error: unknown) { + const execError = error as { stdout?: string; stderr?: string; message?: string } + console.error('Go test failed:', execError.stdout ?? '', execError.stderr ?? '') + throw error + } +} + +const getModuleConfig = () => ({ + modules: [ + { + type: 'local', + name: 'visualization', + executable_path: path.resolve(dirname, '.bin/visualization'), + }, + ], + services: [ + { + name: 'world-state-store', + api: 'rdk:service:world_state_store', + model: 'viam-viz:visualization:world-state-store', + attributes: {}, + }, + ], + components: [], +}) + +withRobot.beforeAll(async () => { + const config = getE2EConfig() + const viamClient = await connectViamClient() + await viamClient.appClient.updateRobotPart( + config.partId, + config.machineName, + Struct.fromJson(getModuleConfig() as unknown as JsonValue) + ) + + // Give viam-server time to load the module and configure the service + await new Promise((resolve) => setTimeout(resolve, 10_000)) + + // Pushing an unchanged config does not restart the module, and it holds its scene in process, + // so anything a previous run left behind would show up in the first screenshot here. + draw('^TestVisualizationReset$') +}) + +withRobot.afterAll(async () => { + const config = getE2EConfig() + const viamClient = await connectViamClient() + await viamClient.appClient.updateRobotPart(config.partId, config.machineName, Struct.fromJson({})) +}) + +/** + * The entities sit in one row 900mm apart, each inside a ±250mm envelope, so every shape is + * separately legible and a change to one cannot be mistaken for a change to its neighbour. + */ +withRobot('every drawing kind renders, separated', async ({ robotPage }) => { + const { page } = robotPage + + draw('^TestVisualizationLayout$/Line') + await expect(page.getByText('viz-line', { exact: true })).toBeVisible({ timeout: 30_000 }) + await robotPage.screenshotCanvas('VIZ-LAYOUT-0-line') + + draw('^TestVisualizationLayout$/Points') + await expect(page.getByText('viz-points', { exact: true })).toBeVisible({ timeout: 10_000 }) + await robotPage.screenshotCanvas('VIZ-LAYOUT-1-points') + + draw('^TestVisualizationLayout$/Arrows') + await expect(page.getByText('viz-arrows', { exact: true })).toBeVisible({ timeout: 10_000 }) + await robotPage.screenshotCanvas('VIZ-LAYOUT-2-arrows') + + draw('^TestVisualizationLayout$/Nurbs') + await expect(page.getByText('viz-nurbs', { exact: true })).toBeVisible({ timeout: 10_000 }) + await robotPage.screenshotCanvas('VIZ-LAYOUT-3-nurbs') + + // A native Transform, to prove projected drawings and real geometry share one world. + draw('^TestVisualizationLayout$/Box') + await expect(page.getByText('viz-box', { exact: true })).toBeVisible({ timeout: 10_000 }) + await robotPage.screenshotCanvas('VIZ-LAYOUT-4-all-five') + + robotPage.assertScreenshots() +}) + +withRobot('updates change one entity at a time', async ({ robotPage }) => { + const { page } = robotPage + + await expect(page.getByText('viz-line', { exact: true })).toBeVisible({ timeout: 30_000 }) + + // The shallow V becomes a tall spike, well above everything else in the row. + draw('^TestVisualizationUpdate$/ReshapeLine') + await page.waitForTimeout(2000) + await robotPage.screenshotCanvas('VIZ-UPDATE-0-line-reshaped') + + // Green to magenta: a hue jump, not a shade of the same colour. + draw('^TestVisualizationUpdate$/RecolorPoints') + await page.waitForTimeout(2000) + await robotPage.screenshotCanvas('VIZ-UPDATE-1-points-recoloured') + + // The arrows vanish, leaving a hole in the middle of the row. + draw('^TestVisualizationUpdate$/HideArrows') + await page.waitForTimeout(2000) + await robotPage.screenshotCanvas('VIZ-UPDATE-2-arrows-hidden') + + draw('^TestVisualizationUpdate$/ShowArrows') + await page.waitForTimeout(2000) + await robotPage.screenshotCanvas('VIZ-UPDATE-3-arrows-shown') + + // A pose change on a native Transform: the box lifts a metre clear of the row. + draw('^TestVisualizationUpdate$/RaiseBox') + await page.waitForTimeout(2000) + await robotPage.screenshotCanvas('VIZ-UPDATE-4-box-raised') + + robotPage.assertScreenshots() +}) + +/** + * 2,000 points delivered 200 at a time. The seed screenshot shows a short stub; the filled one + * shows the full run, so the difference is the chunk pull working. + */ +withRobot('a chunked point cloud fills in', async ({ robotPage }) => { + const { page } = robotPage + + draw('^TestVisualizationChunkedCloud$/Seed') + await expect(page.getByText('viz-cloud', { exact: true })).toBeVisible({ timeout: 30_000 }) + await robotPage.screenshotCanvas('VIZ-CHUNK-0-first-chunk') + + draw('^TestVisualizationChunkedCloud$/Fill') + await page.waitForTimeout(5000) + await robotPage.screenshotCanvas('VIZ-CHUNK-1-filled') + + robotPage.assertScreenshots() +}) + +withRobot('removals take one entity at a time', async ({ robotPage }) => { + const { page } = robotPage + + await expect(page.getByText('viz-line', { exact: true })).toBeVisible({ timeout: 30_000 }) + + draw('^TestVisualizationRemove$/RemoveLine') + await expect(page.getByText('viz-line', { exact: true })).toBeHidden({ timeout: 10_000 }) + // Its neighbours must survive, so the screenshot shows a gap rather than an empty scene. + await expect(page.getByText('viz-points', { exact: true })).toBeVisible() + await robotPage.screenshotCanvas('VIZ-REMOVE-0-line-gone') + + draw('^TestVisualizationRemove$/RemoveBox') + await expect(page.getByText('viz-box', { exact: true })).toBeHidden({ timeout: 10_000 }) + await expect(page.getByText('viz-nurbs', { exact: true })).toBeVisible() + await robotPage.screenshotCanvas('VIZ-REMOVE-1-box-gone') + + // world_state_store has no bulk signal, so this fans out to one REMOVED per entity. + draw('^TestVisualizationRemove$/RemoveAll') + await expect(page.getByText('viz-points', { exact: true })).toBeHidden({ timeout: 10_000 }) + await expect(page.getByText('viz-arrows', { exact: true })).toBeHidden() + await expect(page.getByText('viz-nurbs', { exact: true })).toBeHidden() + await expect(page.getByText('viz-cloud', { exact: true })).toBeHidden() + await robotPage.screenshotCanvas('VIZ-REMOVE-2-empty') + + robotPage.assertScreenshots() +}) diff --git a/e2e/visualization-module.test.ts-snapshots/VIZ-CHUNK-0-first-chunk-darwin.png b/e2e/visualization-module.test.ts-snapshots/VIZ-CHUNK-0-first-chunk-darwin.png new file mode 100644 index 000000000..0240a4513 Binary files /dev/null and b/e2e/visualization-module.test.ts-snapshots/VIZ-CHUNK-0-first-chunk-darwin.png differ diff --git a/e2e/visualization-module.test.ts-snapshots/VIZ-CHUNK-1-filled-darwin.png b/e2e/visualization-module.test.ts-snapshots/VIZ-CHUNK-1-filled-darwin.png new file mode 100644 index 000000000..3be409e2b Binary files /dev/null and b/e2e/visualization-module.test.ts-snapshots/VIZ-CHUNK-1-filled-darwin.png differ diff --git a/e2e/visualization-module.test.ts-snapshots/VIZ-LAYOUT-0-line-darwin.png b/e2e/visualization-module.test.ts-snapshots/VIZ-LAYOUT-0-line-darwin.png new file mode 100644 index 000000000..ccceab430 Binary files /dev/null and b/e2e/visualization-module.test.ts-snapshots/VIZ-LAYOUT-0-line-darwin.png differ diff --git a/e2e/visualization-module.test.ts-snapshots/VIZ-LAYOUT-1-points-darwin.png b/e2e/visualization-module.test.ts-snapshots/VIZ-LAYOUT-1-points-darwin.png new file mode 100644 index 000000000..af0af1efa Binary files /dev/null and b/e2e/visualization-module.test.ts-snapshots/VIZ-LAYOUT-1-points-darwin.png differ diff --git a/e2e/visualization-module.test.ts-snapshots/VIZ-LAYOUT-2-arrows-darwin.png b/e2e/visualization-module.test.ts-snapshots/VIZ-LAYOUT-2-arrows-darwin.png new file mode 100644 index 000000000..7a6865a1e Binary files /dev/null and b/e2e/visualization-module.test.ts-snapshots/VIZ-LAYOUT-2-arrows-darwin.png differ diff --git a/e2e/visualization-module.test.ts-snapshots/VIZ-LAYOUT-3-nurbs-darwin.png b/e2e/visualization-module.test.ts-snapshots/VIZ-LAYOUT-3-nurbs-darwin.png new file mode 100644 index 000000000..2149a9c4d Binary files /dev/null and b/e2e/visualization-module.test.ts-snapshots/VIZ-LAYOUT-3-nurbs-darwin.png differ diff --git a/e2e/visualization-module.test.ts-snapshots/VIZ-LAYOUT-4-all-five-darwin.png b/e2e/visualization-module.test.ts-snapshots/VIZ-LAYOUT-4-all-five-darwin.png new file mode 100644 index 000000000..a9a82f573 Binary files /dev/null and b/e2e/visualization-module.test.ts-snapshots/VIZ-LAYOUT-4-all-five-darwin.png differ diff --git a/e2e/visualization-module.test.ts-snapshots/VIZ-UPDATE-0-line-reshaped-darwin.png b/e2e/visualization-module.test.ts-snapshots/VIZ-UPDATE-0-line-reshaped-darwin.png new file mode 100644 index 000000000..4b0b3c1bb Binary files /dev/null and b/e2e/visualization-module.test.ts-snapshots/VIZ-UPDATE-0-line-reshaped-darwin.png differ diff --git a/e2e/visualization-module.test.ts-snapshots/VIZ-UPDATE-1-points-recoloured-darwin.png b/e2e/visualization-module.test.ts-snapshots/VIZ-UPDATE-1-points-recoloured-darwin.png new file mode 100644 index 000000000..6db31fe33 Binary files /dev/null and b/e2e/visualization-module.test.ts-snapshots/VIZ-UPDATE-1-points-recoloured-darwin.png differ diff --git a/e2e/visualization-module.test.ts-snapshots/VIZ-UPDATE-2-arrows-hidden-darwin.png b/e2e/visualization-module.test.ts-snapshots/VIZ-UPDATE-2-arrows-hidden-darwin.png new file mode 100644 index 000000000..5b1c5b0be Binary files /dev/null and b/e2e/visualization-module.test.ts-snapshots/VIZ-UPDATE-2-arrows-hidden-darwin.png differ diff --git a/e2e/visualization-module.test.ts-snapshots/VIZ-UPDATE-3-arrows-shown-darwin.png b/e2e/visualization-module.test.ts-snapshots/VIZ-UPDATE-3-arrows-shown-darwin.png new file mode 100644 index 000000000..6db31fe33 Binary files /dev/null and b/e2e/visualization-module.test.ts-snapshots/VIZ-UPDATE-3-arrows-shown-darwin.png differ diff --git a/e2e/visualization-module.test.ts-snapshots/VIZ-UPDATE-4-box-raised-darwin.png b/e2e/visualization-module.test.ts-snapshots/VIZ-UPDATE-4-box-raised-darwin.png new file mode 100644 index 000000000..83eb8f187 Binary files /dev/null and b/e2e/visualization-module.test.ts-snapshots/VIZ-UPDATE-4-box-raised-darwin.png differ diff --git a/modules/visualization/README.md b/modules/visualization/README.md new file mode 100644 index 000000000..aebd94757 --- /dev/null +++ b/modules/visualization/README.md @@ -0,0 +1,169 @@ +# Visualization module + +A Viam service that serves the draw API over the [`world_state_store`](https://docs.viam.com/reference/apis/services/world-state-store/) API. + +The draw API's other server, `client/server`, listens on `localhost:3030` with no credentials, which confines drawings to local sessions and custom applications. This module is a peer of that server: the same draw service, reached through a machine instead of a port. Configure it, write to it from any Viam SDK, and the visualizer renders the result. + +## Model + +`viam-viz:visualization:world-state-store`, implementing `rdk:service:world_state_store`. + +### Configuration + +```json +{ + "temp_dir": "/tmp/viam-visualization" +} +``` + +| Attribute | Type | Required | Description | +| ---------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `temp_dir` | string | no | Where chunked entity payloads are buffered. Defaults to `viam-visualization` under the system temp dir. Stale files there are deleted at startup, so do not point it at a directory holding anything else. | + +## Writing + +From Go, build with the `draw` package's shape builders and serialize with the `worldstate` package. Don't hand-write the payload: + +```go +import ( + "github.com/viam-labs/motion-tools/draw" + "github.com/viam-labs/motion-tools/worldstate" +) + +store, err := worldstatestore.FromProvider(machine, "world-state-store") + +line, err := draw.NewLine( + []r3.Vector{{X: -250}, {Z: 200}, {X: 250}}, + draw.WithLineWidth(12), + draw.WithSingleLineColor(draw.NewColor(draw.WithRGB(0, 120, 255))), +) + +// WithID derives a stable UUID from the name, so drawing "my-line" again updates it in place. +drawing := line.Draw("my-line", + draw.WithID("my-line"), + draw.WithPose(spatialmath.NewPoseFromPoint(r3.Vector{Z: 500})), +) + +cmd, err := worldstate.AddDrawingCommand(drawing.ToProto()) +_, err = store.DoCommand(ctx, cmd) +``` + +`worldstate.AddTransformCommand` does the same for a `*commonv1.Transform` from `draw.NewTransform` or `DrawnGeometry.Draw`. `worldstate.UpdateDrawingCommand` and `worldstate.UpdateTransformCommand` take a UUID and the `draw.DrawingPath*` / `draw.TransformPath*` field-mask constants; `worldstate.RemoveEntityCommand`, `worldstate.RemoveAllCommand` and `worldstate.GetEntityChunkCommand` round out the set. For anything without a helper, `worldstate.Command(method, request)` encodes an arbitrary `draw.v1.DrawService` request. + +### The wire form + +Underneath, a command names a `draw.v1.DrawService` method and carries its request as protojson, so the proto is the schema and a new RPC or drawing type needs no change to this module. The helpers above produce this object; any SDK's `DoCommand` can send it built by hand: + +```jsonc +{ + "command": "AddEntity", + "request": { + "drawing": { + "reference_frame": "my-line", + "pose_in_observer_frame": { + "reference_frame": "world", + "pose": { "x": 0, "y": 0, "z": 500, "oz": 1 }, + }, + "physical_object": { + "line": { "positions": "", "line_width": 12 }, + }, + "metadata": { "colors": "", "color_format": 1 }, + }, + }, +} +``` + +From TypeScript, that goes through the same client the visualizer uses: + +```ts +import { WorldStateStoreClient } from '@viamrobotics/sdk' + +const store = new WorldStateStoreClient(machine, 'world-state-store') +await store.doCommand(command) +``` + +Supported commands: `AddEntity`, `AddEntities`, `UpdateEntity`, `RemoveEntity`, `RemoveAll`, `RemoveAllDrawings`, `RemoveAllTransforms`, `CreateRelationship`, `DeleteRelationship`, `GetEntityChunk`, `SetScene`. + +Bytes fields (`positions`, `poses`, `control_points`, `colors`, `opacities`, `uuid`) are base64 strings, as protojson requires. Enums are accepted as numbers or names. + +Streaming methods are not tunnelled — `StreamTransformChanges` replaces them. + +### Chunked entities + +A point set too large for one message is sent as an `AddEntity` carrying the first chunk and declaring `metadata.chunks`, then one `UpdateEntity` per remaining chunk. An **unmasked** `UpdateEntity` on a chunked entity appends to its buffer; a masked one patches a field. The visualizer pulls whatever it has not received with `get_entity_chunk`. + +```go +seed := chunkOf(0).ToProto() +seed.Metadata.Chunks = &drawv1.Chunks{ChunkSize: 200, Total: 2000, Stride: 12} +cmd, err := worldstate.AddDrawingCommand(seed) + +// then, once per remaining chunk — no field mask means "append" +cmd, err = worldstate.UpdateDrawingCommand(uuid, chunkOf(start).ToProto()) +``` + +```jsonc +// first: declares the total, carries chunk 0 +{"command": "AddEntity", "request": {"drawing": { + "reference_frame": "cloud", + "physical_object": {"points": {"positions": ""}}, + "metadata": {"chunks": {"chunk_size": 200, "total": 2000, "stride": 12}} +}}} + +// then, once per remaining chunk — no updated_fields +{"command": "UpdateEntity", "request": {"uuid": "", "drawing": { + "reference_frame": "cloud", + "physical_object": {"points": {"positions": ""}} +}}} +``` + +An append is still validated as an update, so it must repeat `reference_frame`; omitting it is rejected as an attempt to rename the entity. `stride` is bytes per element (12 for float32 xyz) and `total` is element count. + +## Reading + +`ListUUIDs`, `GetTransform` and `StreamTransformChanges` work as the API specifies. Chunked entities are pulled with the command the visualizer already sends: + +```json +{ "command": "get_entity_chunk", "uuid": "<36-char uuid>", "start": 0 } +``` + +### How drawings travel + +`viam.common.v1.Transform.physical_object` is a closed `Geometry` oneof — sphere, box, capsule, mesh, pointcloud — with no case for a line, arrow, points set, NURBS curve or model. So a `Drawing` is projected onto a `Transform`: its reference frame, pose and UUID ride natively, and its `Shape` is encoded as protojson into the metadata `Struct` under a `shape` key, declared by `draw.v1.DrawingProjection`. + +A transform with no `shape` key is a real transform and renders unchanged. The visualizer's `useWorldState` hook decodes the key with the generated `DrawingProjection` and hands the result to the same renderers the local draw service uses, so there is one definition of drawing data and one parser for it. + +## Limits + +- **Bulk clears fan out.** `RemoveAll` is one event inside the draw service but one `REMOVED` per entity over `world_state_store`, which has no bulk signal. +- **No scene or camera channel.** `SetScene` is accepted, but `world_state_store` has no push channel for scene metadata, so camera moves do not reach the visualizer. +- **Chunking covers `points` only.** Other shapes must fit in a single message. +- **32 MiB per message.** That is `rpc.MaxMessageSize`, and protojson base64 inflates binary payloads by about a third — roughly 20 bytes per coloured point. Use `chunks` for large point sets, and prefer `ModelAsset.url` over inline `data`. + +## Development + +``` +./build.sh # builds bin/visualization +go test ./... -count=1 +``` + +Register it as a local module while developing: + +```json +{ + "modules": [ + { + "type": "local", + "name": "visualization", + "executable_path": "/absolute/path/to/modules/visualization/bin/visualization" + } + ], + "services": [ + { + "name": "world-state-store", + "api": "rdk:service:world_state_store", + "model": "viam-viz:visualization:world-state-store", + "attributes": {} + } + ] +} +``` diff --git a/modules/visualization/build.sh b/modules/visualization/build.sh new file mode 100755 index 000000000..55f0c998c --- /dev/null +++ b/modules/visualization/build.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" +mkdir -p bin +go build -o bin/visualization . diff --git a/modules/visualization/do_command.go b/modules/visualization/do_command.go new file mode 100644 index 000000000..aff2b8535 --- /dev/null +++ b/modules/visualization/do_command.go @@ -0,0 +1,189 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "connectrpc.com/connect" + "github.com/google/uuid" + drawv1 "github.com/viam-labs/motion-tools/draw/v1" + "github.com/viam-labs/motion-tools/worldstate" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" +) + +// ErrMissingCommand is returned when a DoCommand payload names no command. +var ErrMissingCommand = errors.New(`DoCommand requires a "command" string`) + +// Proto field names make a hand-written request read like the .proto file, and numeric enums +// keep the metadata Struct contract identical on both halves of the wire. +var ( + tunnelMarshal = protojson.MarshalOptions{UseProtoNames: true, UseEnumNumbers: true} + tunnelUnmarshal = protojson.UnmarshalOptions{DiscardUnknown: true} +) + +type commandHandler func(context.Context, any) (map[string]any, error) + +// DoCommand is the module's write door and its chunked-read door. +// +// A write names a draw.v1.DrawService method and carries its request as protojson, so draw.v1 +// stays the single definition of the vocabulary — a new RPC or drawing type needs no change here: +// +// {"command": "AddEntity", "request": {"drawing": {"reference_frame": "my-line", ...}}} +// +// Streaming methods are not tunnelled; StreamTransformChanges replaces them. Chunked reads keep +// the flat shape the visualizer already sends: +// +// {"command": "get_entity_chunk", "uuid": "<36-char uuid>", "start": } +func (s *Store) DoCommand(ctx context.Context, cmd map[string]any) (map[string]any, error) { + name, ok := cmd[worldstate.CommandKey].(string) + if !ok || name == "" { + return nil, ErrMissingCommand + } + + if name == worldstate.CommandGetEntityChunkFlat { + return s.getEntityChunk(ctx, cmd) + } + + handler, ok := s.commands[name] + if !ok { + return nil, fmt.Errorf("unknown command %q", name) + } + + return handler(ctx, cmd[worldstate.RequestKey]) +} + +func (s *Store) newCommands() map[string]commandHandler { + return map[string]commandHandler{ + worldstate.CommandAddEntity: tunnel(s.svc.AddEntity), + worldstate.CommandAddEntities: tunnel(s.svc.AddEntities), + worldstate.CommandUpdateEntity: tunnel(s.svc.UpdateEntity), + worldstate.CommandRemoveEntity: tunnel(s.svc.RemoveEntity), + worldstate.CommandRemoveAll: tunnel(s.svc.RemoveAll), + worldstate.CommandRemoveAllDrawings: tunnel(s.svc.RemoveAllDrawings), + worldstate.CommandRemoveAllTransforms: tunnel(s.svc.RemoveAllTransforms), + worldstate.CommandCreateRelationship: tunnel(s.svc.CreateRelationship), + worldstate.CommandDeleteRelationship: tunnel(s.svc.DeleteRelationship), + worldstate.CommandSetScene: tunnel(s.svc.SetScene), + worldstate.CommandGetEntityChunk: tunnel(s.svc.GetEntityChunk), + } +} + +// protoPtr constrains a pointer type whose element is T and which is a proto message. +type protoPtr[T any] interface { + *T + proto.Message +} + +// unaryRPC is the shape every non-streaming DrawService method shares. +type unaryRPC[Req, Res any] func(context.Context, *connect.Request[Req]) (*connect.Response[Res], error) + +// tunnel adapts a unary DrawService method into a DoCommand handler, decoding its request and +// encoding its response as protojson. +func tunnel[Req, Res any, ReqPtr protoPtr[Req], ResPtr protoPtr[Res]]( + call unaryRPC[Req, Res], +) commandHandler { + return func(ctx context.Context, payload any) (map[string]any, error) { + var request Req + if err := decodeRequest(payload, ReqPtr(&request)); err != nil { + return nil, err + } + + response, err := call(ctx, connect.NewRequest(&request)) + if err != nil { + return nil, err + } + + return encodeMessage(ResPtr(response.Msg)) + } +} + +// getEntityChunk serves the flat request shape, so the visualizer's existing chunk loader works +// against this module unchanged. +func (s *Store) getEntityChunk(ctx context.Context, cmd map[string]any) (map[string]any, error) { + id, ok := cmd["uuid"].(string) + if !ok || id == "" { + return nil, fmt.Errorf("%s requires a uuid", worldstate.CommandGetEntityChunkFlat) + } + + parsed, err := uuid.Parse(id) + if err != nil { + return nil, fmt.Errorf("parsing uuid %q: %w", id, err) + } + + start, _ := cmd["start"].(float64) + + response, err := s.svc.GetEntityChunk(ctx, connect.NewRequest(&drawv1.GetEntityChunkRequest{ + Uuid: parsed[:], + Start: uint32(start), + })) + if err != nil { + return nil, err + } + + chunk := map[string]any{ + "start": float64(response.Msg.GetStart()), + "done": response.Msg.GetDone(), + } + + if entity := chunkEntity(response.Msg); entity != nil { + encoded, err := encodeMessage(entity) + if err != nil { + return nil, err + } + chunk["entity"] = encoded + } + + return chunk, nil +} + +func chunkEntity(response *drawv1.GetEntityChunkResponse) proto.Message { + switch entity := response.GetEntity().(type) { + case *drawv1.GetEntityChunkResponse_Drawing: + return entity.Drawing + case *drawv1.GetEntityChunkResponse_Transform: + return entity.Transform + default: + return nil + } +} + +// decodeRequest fills msg from a DoCommand payload. A nil payload leaves msg zero, which is what +// the several no-argument requests want. +func decodeRequest(payload any, msg proto.Message) error { + if payload == nil { + return nil + } + + fields, ok := payload.(map[string]any) + if !ok { + return fmt.Errorf("%q must be an object", worldstate.RequestKey) + } + + encoded, err := json.Marshal(fields) + if err != nil { + return fmt.Errorf("re-encoding request: %w", err) + } + + if err := tunnelUnmarshal.Unmarshal(encoded, msg); err != nil { + return fmt.Errorf("decoding %s: %w", msg.ProtoReflect().Descriptor().FullName(), err) + } + + return nil +} + +func encodeMessage(msg proto.Message) (map[string]any, error) { + encoded, err := tunnelMarshal.Marshal(msg) + if err != nil { + return nil, fmt.Errorf("encoding %s: %w", msg.ProtoReflect().Descriptor().FullName(), err) + } + + decoded := map[string]any{} + if err := json.Unmarshal(encoded, &decoded); err != nil { + return nil, fmt.Errorf("re-encoding response: %w", err) + } + + return decoded, nil +} diff --git a/modules/visualization/go.mod b/modules/visualization/go.mod new file mode 100644 index 000000000..3193e6051 --- /dev/null +++ b/modules/visualization/go.mod @@ -0,0 +1,220 @@ +module github.com/viam-labs/motion-tools/modules/visualization + +go 1.25.10 + +replace github.com/viam-labs/motion-tools => ../../ + +require ( + connectrpc.com/connect v1.20.0 + github.com/google/uuid v1.6.0 + github.com/viam-labs/motion-tools v1.35.1 + go.viam.com/api v0.1.537 + go.viam.com/rdk v0.122.0 + go.viam.com/test v1.2.4 + go.viam.com/utils v0.10.1 + google.golang.org/protobuf v1.36.12 +) + +require ( + cel.dev/expr v0.25.2 // indirect + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.18.2 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/iam v1.5.3 // indirect + cloud.google.com/go/monitoring v1.24.3 // indirect + cloud.google.com/go/storage v1.61.3 // indirect + codeberg.org/go-fonts/liberation v0.5.0 // indirect + codeberg.org/go-latex/latex v0.1.0 // indirect + codeberg.org/go-pdf/fpdf v0.10.0 // indirect + git.sr.ht/~sbinet/gg v0.6.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect + github.com/a8m/envsubst v1.4.2 // indirect + github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect + github.com/apache/arrow/go/arrow v0.0.0-20201229220542-30ce2eb5d4dc // indirect + github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.12 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect + github.com/aws/smithy-go v1.24.2 // indirect + github.com/aybabtme/uniplot v0.0.0-20151203143629-039c559e5e7e // indirect + github.com/benbjohnson/clock v1.3.5 // indirect + github.com/bep/debounce v1.2.1 // indirect + github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect + github.com/bluenviron/gortsplib/v4 v4.8.0 // indirect + github.com/bufbuild/protocompile v0.14.2-0.20260605203730-cd7c3c124e10 // indirect + github.com/campoy/embedmd v1.0.0 // indirect + github.com/cenkalti/backoff v2.2.1+incompatible // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chenzhekl/goply v0.0.0-20190930133256-258c2381defd // indirect + github.com/chewxy/hm v1.0.0 // indirect + github.com/chewxy/math32 v1.0.8 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.5.0 // indirect + github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect + github.com/dgottlieb/smarty-assertions v1.2.6 // indirect + github.com/disintegration/imaging v1.6.2 // indirect + github.com/edaniels/golog v0.0.0-20250821172758-0d08e67686a9 // indirect + github.com/edaniels/lidario v0.0.0-20220607182921-5879aa7b96dd // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect + github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect + github.com/felixge/httpsnoop v1.1.0 // indirect + github.com/fogleman/gg v1.3.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fullstorydev/grpcurl v1.8.6 // indirect + github.com/go-gl/mathgl v1.0.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/gonuts/binary v0.2.0 // indirect + github.com/google/flatbuffers v2.0.6+incompatible // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect + github.com/googleapis/gax-go/v2 v2.17.0 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect + github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-getter v1.8.6 // indirect + github.com/hashicorp/go-version v1.8.0 // indirect + github.com/improbable-eng/grpc-web v0.15.0 // indirect + github.com/jedib0t/go-pretty/v6 v6.4.6 // indirect + github.com/jhump/protoreflect v1.15.6 // indirect + github.com/kellydunn/golang-geo v0.7.0 // indirect + github.com/klauspost/compress v1.18.6 // indirect + github.com/klauspost/cpuid/v2 v2.4.0 // indirect + github.com/kylelemons/go-gypsy v1.0.0 // indirect + github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect + github.com/lestrrat-go/blackmagic v1.0.2 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/jwx v1.2.29 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/lmittmann/ppm v1.0.2 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/miekg/dns v1.1.53 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/muesli/clusters v0.0.0-20200529215643-2700303c1762 // indirect + github.com/muesli/kmeans v0.3.1 // indirect + github.com/muhlemmer/gu v0.3.1 // indirect + github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect + github.com/pion/datachannel v1.5.10 // indirect + github.com/pion/dtls/v2 v2.2.12 // indirect + github.com/pion/interceptor v0.1.42 // indirect + github.com/pion/logging v0.2.4 // indirect + github.com/pion/mdns v0.0.12 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtcp v1.2.16 // indirect + github.com/pion/rtp v1.8.26 // indirect + github.com/pion/sctp v1.8.41 // indirect + github.com/pion/sdp/v3 v3.0.16 // indirect + github.com/pion/srtp/v2 v2.0.20 // indirect + github.com/pion/stun v0.6.1 // indirect + github.com/pion/transport/v2 v2.2.10 // indirect + github.com/pion/transport/v3 v3.1.1 // indirect + github.com/pion/turn/v2 v2.1.6 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rs/cors v1.11.1 // indirect + github.com/samber/lo v1.51.0 // indirect + github.com/sergi/go-diff v1.4.0 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect + github.com/srikrsna/protoc-gen-gotag v0.6.2 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/ulikunitz/xz v0.5.15 // indirect + github.com/viamrobotics/ice/v2 v2.3.40 // indirect + github.com/viamrobotics/webrtc/v3 v3.99.16 // indirect + github.com/viamrobotics/zeroconf v1.0.13 // indirect + github.com/wlynxg/anet v0.0.5 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/xfmoulet/qoi v0.2.0 // indirect + github.com/xtgo/set v1.0.0 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + github.com/zhuyie/golzf v0.0.0-20161112031142-8387b0307ade // indirect + github.com/zitadel/oidc/v3 v3.37.0 // indirect + github.com/zitadel/schema v1.3.1 // indirect + github.com/ziutek/mymysql v1.5.4 // indirect + go-hep.org/x/hep v0.32.1 // indirect + go.mongodb.org/mongo-driver v1.17.7 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/sdk v1.44.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.uber.org/goleak v1.3.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.28.0 // indirect + go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect + golang.org/x/crypto v0.53.0 // indirect + golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect + golang.org/x/image v0.41.0 // indirect + golang.org/x/mod v0.37.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.38.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.46.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + gonum.org/v1/gonum v0.17.0 // indirect + gonum.org/v1/plot v0.15.2 // indirect + google.golang.org/api v0.271.0 // indirect + google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect + google.golang.org/grpc v1.82.1 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorgonia.org/tensor v0.9.24 // indirect + gorgonia.org/vecf32 v0.9.0 // indirect + gorgonia.org/vecf64 v0.9.0 // indirect + nhooyr.io/websocket v1.8.7 // indirect +) diff --git a/modules/visualization/go.sum b/modules/visualization/go.sum new file mode 100644 index 000000000..afb869d4c --- /dev/null +++ b/modules/visualization/go.sum @@ -0,0 +1,1344 @@ +buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1 h1:HwzzCRS4ZrEm1++rzSDxHnO0DOjiT1b8I/24e8a4exY= +buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1/go.mod h1:8PRKXhgNes29Tjrnv8KdZzg3I1QceOkzibW1QK7EXv0= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20240920164238-5a7b106cbb87.1 h1:5Hare6GmKrTUFXYxNwOrZyXtGTzKUMXTRHdNxQkewcE= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20240920164238-5a7b106cbb87.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= +cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= +cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= +cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/container v1.45.0 h1:i1No5obpPxlIFLGHdUF6h2YjRR1qN9t/ZkA8KA5B//o= +cloud.google.com/go/container v1.45.0/go.mod h1:eB6jUfJLjne9VsTDGcH7mnj6JyZK+KOUIA6KZnYE/ds= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= +cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= +cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY= +cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw= +cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= +cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= +cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= +cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.61.3 h1:VS//ZfBuPGDvakfD9xyPW1RGF1Vy3BWUoVZXgW1KMOg= +cloud.google.com/go/storage v1.61.3/go.mod h1:JtqK8BBB7TWv0HVGHubtUdzYYrakOQIsMLffZ2Z/HWk= +cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= +cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= +codeberg.org/go-fonts/dejavu v0.4.0 h1:2yn58Vkh4CFK3ipacWUAIE3XVBGNa0y1bc95Bmfx91I= +codeberg.org/go-fonts/dejavu v0.4.0/go.mod h1:abni088lmhQJvso2Lsb7azCKzwkfcnttl6tL1UTWKzg= +codeberg.org/go-fonts/latin-modern v0.4.0 h1:vkRCc1y3whKA7iL9Ep0fSGVuJfqjix0ica9UflHORO8= +codeberg.org/go-fonts/latin-modern v0.4.0/go.mod h1:BF68mZznJ9QHn+hic9ks2DaFl4sR5YhfM6xTYaP9vNw= +codeberg.org/go-fonts/liberation v0.5.0 h1:SsKoMO1v1OZmzkG2DY+7ZkCL9U+rrWI09niOLfQ5Bo0= +codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU= +codeberg.org/go-latex/latex v0.1.0 h1:hoGO86rIbWVyjtlDLzCqZPjNykpWQ9YuTZqAzPcfL3c= +codeberg.org/go-latex/latex v0.1.0/go.mod h1:LA0q/AyWIYrqVd+A9Upkgsb+IqPcmSTKc9Dny04MHMw= +codeberg.org/go-pdf/fpdf v0.10.0 h1:u+w669foDDx5Ds43mpiiayp40Ov6sZalgcPMDBcZRd4= +codeberg.org/go-pdf/fpdf v0.10.0/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU= +connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ= +connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= +contrib.go.opencensus.io/exporter/jaeger v0.2.1 h1:yGBYzYMewVL0yO9qqJv3Z5+IRhPdU7e9o/2oKpX4YvI= +contrib.go.opencensus.io/exporter/jaeger v0.2.1/go.mod h1:Y8IsLgdxqh1QxYxPC5IgXVmBaeLUeQFfBeBi9PbeZd0= +contrib.go.opencensus.io/exporter/stackdriver v0.13.4 h1:ksUxwH3OD5sxkjzEqGxNTl+Xjsmu3BnC/300MhSVTSc= +contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +git.sr.ht/~sbinet/cmpimg v0.1.0 h1:E0zPRk2muWuCqSKSVZIWsgtU9pjsw3eKHi8VmQeScxo= +git.sr.ht/~sbinet/cmpimg v0.1.0/go.mod h1:FU12psLbF4TfNXkKH2ZZQ29crIqoiqTZmeQ7dkp/pxE= +git.sr.ht/~sbinet/gg v0.6.0 h1:RIzgkizAk+9r7uPzf/VfbJHBMKUr0F5hRFxTUGMnt38= +git.sr.ht/~sbinet/gg v0.6.0/go.mod h1:uucygbfC9wVPQIfrmwM2et0imr8L7KQWywX0xpFMm94= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/a8m/envsubst v1.4.2 h1:4yWIHXOLEJHQEFd4UjrWDrYeYlV7ncFWJOCBRLOZHQg= +github.com/a8m/envsubst v1.4.2/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= +github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apache/arrow/go/arrow v0.0.0-20201229220542-30ce2eb5d4dc h1:zvQ6w7KwtQWgMQiewOF9tFtundRMVZFSAksNV6ogzuY= +github.com/apache/arrow/go/arrow v0.0.0-20201229220542-30ce2eb5d4dc/go.mod h1:c9sxoIT3YgLxH4UhLOCKaBlEojuMhVYpk4Ntv3opUTQ= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.38.20 h1:QbzNx/tdfATbdKfubBpkt84OM6oBkxQZRw6+bW2GyeA= +github.com/aws/aws-sdk-go v1.38.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= +github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= +github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= +github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ= +github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo= +github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= +github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= +github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aybabtme/uniplot v0.0.0-20151203143629-039c559e5e7e h1:dSeuFcs4WAJJnswS8vXy7YY1+fdlbVPuEVmDAfqvFOQ= +github.com/aybabtme/uniplot v0.0.0-20151203143629-039c559e5e7e/go.mod h1:uh71c5Vc3VNIplXOFXsnDy21T1BepgT32c5X/YPrOyc= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bluenviron/gortsplib/v4 v4.8.0 h1:nvFp6rHALcSep3G9uBFI0uogS9stVZLNq/92TzGZdQg= +github.com/bluenviron/gortsplib/v4 v4.8.0/go.mod h1:+d+veuyvhvikUNp0GRQkk6fEbd/DtcXNidMRm7FQRaA= +github.com/bufbuild/protocompile v0.14.2-0.20260605203730-cd7c3c124e10 h1:N0ZOKMwHpKsFhMGSxnB6JiVkM7SbH8IAKlGGO0SpQBA= +github.com/bufbuild/protocompile v0.14.2-0.20260605203730-cd7c3c124e10/go.mod h1:jPUiZUFWc8E3Kc2Y4SRlGAdjde4amGkHY0BUACNS43E= +github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g= +github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= +github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= +github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/caarlos0/env/v11 v11.4.0 h1:Kcb6t5kIIr4XkoQC9AF2j+8E1Jsrl3Wz/hhm1LtoGAc= +github.com/caarlos0/env/v11 v11.4.0/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= +github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY= +github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chenzhekl/goply v0.0.0-20190930133256-258c2381defd h1:S0onsSZ3RawTrm4KrxPs7KoPT8R8aBZmbXvOUGkV5O8= +github.com/chenzhekl/goply v0.0.0-20190930133256-258c2381defd/go.mod h1:P2dOeu3SNXtjA5VOH7tF0AnGm/eYrst9YA89b36c35I= +github.com/chewxy/hm v1.0.0 h1:zy/TSv3LV2nD3dwUEQL2VhXeoXbb9QkpmdRAVUFiA6k= +github.com/chewxy/hm v1.0.0/go.mod h1:qg9YI4q6Fkj/whwHR1D+bOGeF7SniIP40VweVepLjg0= +github.com/chewxy/math32 v1.0.0/go.mod h1:Miac6hA1ohdDUTagnvJy/q+aNnEk16qWUdb8ZVhvCN0= +github.com/chewxy/math32 v1.0.8 h1:fU5E4Ec4Z+5RtRAi3TovSxUjQPkgRh+HbP7tKB2OFbM= +github.com/chewxy/math32 v1.0.8/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= +github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= +github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= +github.com/dgottlieb/smarty-assertions v1.2.6 h1:YAXgSslRBbVtd54iTqM4yGT2k1a2qS6cffNQo0SDxDY= +github.com/dgottlieb/smarty-assertions v1.2.6/go.mod h1:x1wpV/RTxYWtN+vgrcRuCF4hjUmonK5NR59ZzQSym2k= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edaniels/golog v0.0.0-20250821172758-0d08e67686a9 h1:/HeoZScYwEZburQ/HMRt8xM3RRsfyCvUdMhGsEQl8B8= +github.com/edaniels/golog v0.0.0-20250821172758-0d08e67686a9/go.mod h1:66V//s+5fy74xUPs7VMhMST5AleWWK/s6bOwgbkiQik= +github.com/edaniels/lidario v0.0.0-20220607182921-5879aa7b96dd h1:W6Zlh2ja8A5vljn17Ix/gZhaUbYMFKAuBjc4t9nma7U= +github.com/edaniels/lidario v0.0.0-20220607182921-5879aa7b96dd/go.mod h1:CibiLtefSZOd0smxazen6pzvjNK3HrvgGojYOqC3/4Q= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= +github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fullstorydev/grpcurl v1.8.6 h1:WylAwnPauJIofYSHqqMTC1eEfUIzqzevXyogBxnQquo= +github.com/fullstorydev/grpcurl v1.8.6/go.mod h1:WhP7fRQdhxz2TkL97u+TCb505sxfH78W1usyoB3tepw= +github.com/gen2brain/malgo v0.11.24 h1:hHcIJVfzWcEDHFdPl5Dl/CUSOjzOleY0zzAV8Kx+imE= +github.com/gen2brain/malgo v0.11.24/go.mod h1:f9TtuN7DVrXMiV/yIceMeWpvanyVzJQMlBecJFVMxww= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-co-op/gocron/v2 v2.18.0 h1:DS3Uhru66q1jy/5f9V0itmi3cLXcn2b7N+duGfgT7gU= +github.com/go-co-op/gocron/v2 v2.18.0/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/mathgl v1.0.0 h1:t9DznWJlXxxjeeKLIdovCOVJQk/GzDEL7h/h+Ro2B68= +github.com/go-gl/mathgl v1.0.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= +github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/goccy/go-graphviz v0.1.3 h1:Pkt8y4FBnBNI9tfSobpoN5qy1qMNqRXPQYvLhaSUasY= +github.com/goccy/go-graphviz v0.1.3/go.mod h1:pMYpbAqJT10V8dzV1JN/g/wUlG/0imKPzn3ZsrchGCI= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I= +github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gonuts/binary v0.2.0 h1:caITwMWAoQWlL0RNvv2lTU/AHqAJlVuu6nZmNgfbKW4= +github.com/gonuts/binary v0.2.0/go.mod h1:kM+CtBrCGDSKdv8WXTuCUsw+loiy8f/QEI8YCCC0M/E= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v2.0.6+incompatible h1:XHFReMv7nFFusa+CEokzWbzaYocKXI6C7hdU5Kgh9Lw= +github.com/google/flatbuffers v2.0.6+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= +github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= +github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72 h1:vTCWu1wbdYo7PEZFem/rlr01+Un+wwVmI7wiegFdRLk= +github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72/go.mod h1:Vn+BBgKQHVQYdVQ4NZDICE1Brb+JfaONyDHr3q07oQc= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-getter v1.8.6 h1:9sQboWULaydVphxc4S64oAI4YqpuCk7nPmvbk131ebY= +github.com/hashicorp/go-getter v1.8.6/go.mod h1:nVH12eOV2P58dIiL3rsU6Fh3wLeJEKBOJzhMmzlSWoo= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= +github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= +github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jedib0t/go-pretty/v6 v6.4.6 h1:v6aG9h6Uby3IusSSEjHaZNXpHFhzqMmjXcPq1Rjl9Jw= +github.com/jedib0t/go-pretty/v6 v6.4.6/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs= +github.com/jhump/protoreflect v1.10.3/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg= +github.com/jhump/protoreflect v1.15.6 h1:WMYJbw2Wo+KOWwZFvgY0jMoVHM6i4XIvRs2RcBj5VmI= +github.com/jhump/protoreflect v1.15.6/go.mod h1:jCHoyYQIJnaabEYnbGwyo9hUqfyUMTbJw/tAut5t97E= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kellydunn/golang-geo v0.7.0 h1:A5j0/BvNgGwY6Yb6inXQxzYwlPHc6WVZR+MrarZYNNg= +github.com/kellydunn/golang-geo v0.7.0/go.mod h1:YYlQPJ+DPEzrHx8kT3oPHC/NjyvCCXE+IuKGKdrjrcU= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/go-gypsy v1.0.0 h1:7/wQ7A3UL1bnqRMnZ6T8cwCOArfZCxFmb1iTxaOOo1s= +github.com/kylelemons/go-gypsy v1.0.0/go.mod h1:chkXM0zjdpXOiqkCW1XcCHDfjfk14PH2KKkQWxfJUcU= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx v1.2.29 h1:QT0utmUJ4/12rmsVQrJ3u55bycPkKqGYuGT4tyRhxSQ= +github.com/lestrrat-go/jwx v1.2.29/go.mod h1:hU8k2l6WF0ncx20uQdOmik/Gjg6E3/wIRtXSNFeZuB8= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lmittmann/ppm v1.0.2 h1:YW2FFG864rGdrzYu41XngKfptOQU2V+cOmi/hBbaUlI= +github.com/lmittmann/ppm v1.0.2/go.mod h1:GObNM/dbtplb87+9xClwI9bZ+AOPMg0Ujf4k3iLo23E= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw= +github.com/miekg/dns v1.1.53/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/muesli/clusters v0.0.0-20180605185049-a07a36e67d36/go.mod h1:mw5KDqUj0eLj/6DUNINLVJNoPTFkEuGMHtJsXLviLkY= +github.com/muesli/clusters v0.0.0-20200529215643-2700303c1762 h1:p4A2Jx7Lm3NV98VRMKlyWd3nqf8obft8NfXlAUmqd3I= +github.com/muesli/clusters v0.0.0-20200529215643-2700303c1762/go.mod h1:mw5KDqUj0eLj/6DUNINLVJNoPTFkEuGMHtJsXLviLkY= +github.com/muesli/kmeans v0.3.1 h1:KshLQ8wAETfLWOJKMuDCVYHnafddSa1kwGh/IypGIzY= +github.com/muesli/kmeans v0.3.1/go.mod h1:8/OvJW7cHc1BpRf8URb43m+vR105DDe+Kj1WcFXYDqc= +github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= +github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/grpc-proxy v0.0.0-20181017164139-0f1106ef9c76/go.mod h1:x5OoJHDHqxHS801UIuhqGl6QdSAEJvtausosHSdazIo= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/nishanths/predeclared v0.0.0-20200524104333-86fad755b4d3/go.mod h1:nt3d53pc1VYcphSCIaYAJtnPYnr3Zyn8fMq2wvPGPso= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM= +github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= +github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= +github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= +github.com/pion/dtls/v3 v3.0.11 h1:zqn8YhoAU7d9whsWLhNiQlbB8QdpJj8XQVSc5ImUons= +github.com/pion/dtls/v3 v3.0.11/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8= +github.com/pion/ice/v4 v4.0.13 h1:1cdmd80gmLdnVTM2bXzw2CBebvXvkGNEaWi/CuDK9WQ= +github.com/pion/ice/v4 v4.0.13/go.mod h1:Xo5f5DBbEjQac+6pR7i83AGuwoGxnxwXkOOvHFVnfnM= +github.com/pion/interceptor v0.1.42 h1:0/4tvNtruXflBxLfApMVoMubUMik57VZ+94U0J7cmkQ= +github.com/pion/interceptor v0.1.42/go.mod h1:g6XYTChs9XyolIQFhRHOOUS+bGVGLRfgTCUzH29EfVU= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= +github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk= +github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY= +github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A= +github.com/pion/mediadevices v0.9.0 h1:3ozqxVjN0PTbb1IfTbZ4af4R/fQ5cSlbYL3euCZag7w= +github.com/pion/mediadevices v0.9.0/go.mod h1:0dGJQq8VCPo7AXWmhqRITIFyw66uylwDecq7oN+G3gM= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= +github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= +github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= +github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= +github.com/pion/rtp v1.8.26 h1:VB+ESQFQhBXFytD+Gk8cxB6dXeVf2WQzg4aORvAvAAc= +github.com/pion/rtp v1.8.26/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM= +github.com/pion/sctp v1.8.41 h1:20R4OHAno4Vky3/iE4xccInAScAa83X6nWUfyc65MIs= +github.com/pion/sctp v1.8.41/go.mod h1:2wO6HBycUH7iCssuGyc2e9+0giXVW0pyCv3ZuL8LiyY= +github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo= +github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo= +github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk= +github.com/pion/srtp/v2 v2.0.20/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA= +github.com/pion/srtp/v3 v3.0.9 h1:lRGF4G61xxj+m/YluB3ZnBpiALSri2lTzba0kGZMrQY= +github.com/pion/srtp/v3 v3.0.9/go.mod h1:E+AuWd7Ug2Fp5u38MKnhduvpVkveXJX6J4Lq4rxUYt8= +github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= +github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= +github.com/pion/stun/v3 v3.0.2 h1:BJuGEN2oLrJisiNEJtUTJC4BGbzbfp37LizfqswblFU= +github.com/pion/stun/v3 v3.0.2/go.mod h1:JFJKfIWvt178MCF5H/YIgZ4VX3LYE77vca4b9HP60SA= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= +github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= +github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= +github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= +github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM= +github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= +github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= +github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= +github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc= +github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= +github.com/pion/turn/v4 v4.1.3 h1:jVNW0iR05AS94ysEtvzsrk3gKs9Zqxf6HmnsLfRvlzA= +github.com/pion/turn/v4 v4.1.3/go.mod h1:TD/eiBUf5f5LwXbCJa35T7dPtTpCHRJ9oJWmyPLVT3A= +github.com/pion/webrtc/v4 v4.1.8 h1:ynkjfiURDQ1+8EcJsoa60yumHAmyeYjz08AaOuor+sk= +github.com/pion/webrtc/v4 v4.1.8/go.mod h1:KVaARG2RN0lZx0jc7AWTe38JpPv+1/KicOZ9jN52J/s= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9 h1:arwj11zP0yJIxIRiDn22E0H8PxfF7TsTrc2wIPFIsf4= +github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9/go.mod h1:SKZx6stCn03JN3BOWTwvVIO2ajMkb/zQdTceXYhKw/4= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI= +github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= +github.com/spf13/afero v1.5.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= +github.com/srikrsna/protoc-gen-gotag v0.6.2 h1:ULdarjI7FNUA6CNlLPIzSNvjdV2P4C2LSygPLvCVtfA= +github.com/srikrsna/protoc-gen-gotag v0.6.2/go.mod h1:cplWV0ZNBhuF54gnj6rU9pLNrqjXf5vh65Xqa1Kjy+4= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA= +github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/uber/jaeger-client-go v2.25.0+incompatible h1:IxcNZ7WRY1Y3G4poYlx24szfsn/3LvK9QHCq9oQw8+U= +github.com/uber/jaeger-client-go v2.25.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/viamrobotics/ice/v2 v2.3.40 h1:H9r4ztsKkxWSn42R4fLvYlaPtpBys1Yj7/MERBtZY0k= +github.com/viamrobotics/ice/v2 v2.3.40/go.mod h1:MFqB81U1pliDKaXJx2hGHvYKg1PepyEYwEY10828Ve0= +github.com/viamrobotics/webrtc/v3 v3.99.16 h1:N3OQoWnV9Zg68mTDOq8ymsx2AMpbEjuo1gHWaVd7wB0= +github.com/viamrobotics/webrtc/v3 v3.99.16/go.mod h1:w1LmHsQFaq7vqLh06ybQ5kmYXo8myQq+3Pj073RL0f0= +github.com/viamrobotics/zeroconf v1.0.13 h1:b3SzT1OrWjAWa/IReeehU++NWVp6r9K9/qBbjPZVFXw= +github.com/viamrobotics/zeroconf v1.0.13/go.mod h1:iYvk00hmrzBI1Qw13y5palYDorz5ebndUJyTo7Oa3pc= +github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= +github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xfmoulet/qoi v0.2.0 h1:+Smrwzy5ptRnPzGm/YHkZfyK9qGUSoOpiEPngGmFv+c= +github.com/xfmoulet/qoi v0.2.0/go.mod h1:uuPUygmV7o8qy7PhiaGAQX0iLiqoUvFEUKjwUFtlaTQ= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xtgo/set v1.0.0 h1:6BCNBRv3ORNDQ7fyoJXRv+tstJz3m1JVFQErfeZz2pY= +github.com/xtgo/set v1.0.0/go.mod h1:d3NHzGzSa0NmB2NhFyECA+QdRp29oEn2xbT+TpeFoM8= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zhuyie/golzf v0.0.0-20161112031142-8387b0307ade h1:bafvQukPrIYwYWcft4rl3WpHo3qO0/voaAgnCwgdhi0= +github.com/zhuyie/golzf v0.0.0-20161112031142-8387b0307ade/go.mod h1:juNhYdla04C276MyU4zR0BA7t90ziLKPwkjDgddGYV0= +github.com/zitadel/oidc/v3 v3.37.0 h1:nYATWlnP7f18XiAbw6upUruBaqfB1kUrXrSTf1EYGO8= +github.com/zitadel/oidc/v3 v3.37.0/go.mod h1:/xDan4OUQhguJ4Ur73OOJrtugvR164OMnidXP9xfVNw= +github.com/zitadel/schema v1.3.1 h1:QT3kwiRIRXXLVAs6gCK/u044WmUVh6IlbLXUsn6yRQU= +github.com/zitadel/schema v1.3.1/go.mod h1:071u7D2LQacy1HAN+YnMd/mx1qVE2isb0Mjeqg46xnU= +github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +go-hep.org/x/hep v0.32.1 h1:O96fOyMP+4ET8X+Uu38VFdegQb7rL0rjmFqXMCSm4VM= +go-hep.org/x/hep v0.32.1/go.mod h1:VX3IVUv0Ku5bgWhE+LxRQ1aT7BmWWxSxQu02hfsoeRI= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.mongodb.org/mongo-driver v1.17.7 h1:a9w+U3Vt67eYzcfq3k/OAv284/uUUkL0uP75VE5rCOU= +go.mongodb.org/mongo-driver v1.17.7/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA= +go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.viam.com/api v0.1.537 h1:KMoahNoyRA29rQAFVkr0e39xDvNio5sash3ba/UdKjo= +go.viam.com/api v0.1.537/go.mod h1:qSrz3j4+QlXvw7ANs1G2fsX532vUQzLBbgaTxMu3lAw= +go.viam.com/rdk v0.122.0 h1:3J5kfdQfyPeNN0mxufa4LCt1qdxlgfdWmpWYFdBtRN4= +go.viam.com/rdk v0.122.0/go.mod h1:VAyXU62zgtIq44ymydH8rEM4/W1Emm1Fj1mfo4og2eU= +go.viam.com/test v1.2.4 h1:JYgZhsuGAQ8sL9jWkziAXN9VJJiKbjoi9BsO33TW3ug= +go.viam.com/test v1.2.4/go.mod h1:zI2xzosHdqXAJ/kFqcN+OIF78kQuTV2nIhGZ8EzvaJI= +go.viam.com/utils v0.10.1 h1:O+ysq9uh99G/Ak+2qCvWQORYqjkcnz7yinutGyawRfE= +go.viam.com/utils v0.10.1/go.mod h1:xqyH9IYdJtT0FDS3fWblj45uuYoG95WaWEfMAjE5Yig= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 h1:WJhcL4p+YeDxmZWg141nRm7XC8IDmhz7lk5GpadO1Sg= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= +goji.io v2.0.2+incompatible h1:uIssv/elbKRLznFUy3Xj4+2Mz/qKhek/9aZQDUMae7c= +goji.io v2.0.2+incompatible/go.mod h1:sbqFwrtqZACxLBTQcdgVjFh54yGVCvwq8+w49MVMMIk= +golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= +golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= +golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M= +golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo= +golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= +golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +gonum.org/v1/plot v0.15.2 h1:Tlfh/jBk2tqjLZ4/P8ZIwGrLEWQSPDLRm/SNWKNXiGI= +gonum.org/v1/plot v0.15.2/go.mod h1:DX+x+DWso3LTha+AdkJEv5Txvi+Tql3KAGkehP0/Ubg= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY= +google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200911024640-645f7a48b24f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= +google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad h1:3iLyITS/sySRwbUKoC7ogfj2Yr1Cjs0pfaRKj5U5HEw= +google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:KdNqO+rCIWgFumrNBSEDlDNrkrQnpkax7Tv1WxNY8V4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad h1:45WmJvIV6C2+O/jjLkPUH+F3aOj/1miDoU2DD0+NWbg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v0.0.0-20200910201057-6591123024b3/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.25.1-0.20200805231151-a709e31e5d12/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorgonia.org/tensor v0.9.24 h1:8ahrfwO4iby+1ILObIqfjJa+wyA2RoCfJSS3LVERSRE= +gorgonia.org/tensor v0.9.24/go.mod h1:1dsOegMm2n1obs69YnVJdp2oPSKx9Q9Tco5i7GEaXRg= +gorgonia.org/vecf32 v0.9.0 h1:PClazic1r+JVJ1dEzRXgeiVl4g1/Hf/w+wUSqnco1Xg= +gorgonia.org/vecf32 v0.9.0/go.mod h1:NCc+5D2oxddRL11hd+pCB1PEyXWOyiQxfZ/1wwhOXCA= +gorgonia.org/vecf64 v0.9.0 h1:bgZDP5x0OzBF64PjMGC3EvTdOoMEcmfAh1VCUnZFm1A= +gorgonia.org/vecf64 v0.9.0/go.mod h1:hp7IOWCnRiVQKON73kkC/AUMtEXyf9kGlVrtPQ9ccVA= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +periph.io/x/conn/v3 v3.7.0 h1:f1EXLn4pkf7AEWwkol2gilCNZ0ElY+bxS4WE2PQXfrA= +periph.io/x/conn/v3 v3.7.0/go.mod h1:ypY7UVxgDbP9PJGwFSVelRRagxyXYfttVh7hJZUHEhg= +periph.io/x/host/v3 v3.8.1-0.20230331112814-9f0d9f7d76db h1:8+HL7DJFofYRhGoK/UdwhzvQj3I2HrKLQ6dkOC66CZY= +periph.io/x/host/v3 v3.8.1-0.20230331112814-9f0d9f7d76db/go.mod h1:rzOLH+2g9bhc6pWZrkCrmytD4igwQ2vxFw6Wn6ZOlLY= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/modules/visualization/main.go b/modules/visualization/main.go new file mode 100644 index 000000000..09e092458 --- /dev/null +++ b/modules/visualization/main.go @@ -0,0 +1,45 @@ +// Package main is the visualization module: a Viam service that serves the draw API over +// world_state_store. +// +// Drawings reach the visualizer through a machine rather than client/server's localhost port. +// See README.md. +package main + +import ( + "context" + + "go.viam.com/rdk/logging" + "go.viam.com/rdk/module" + "go.viam.com/rdk/resource" + "go.viam.com/rdk/services/worldstatestore" +) + +// Model is the registry triple this module publishes. +var Model = resource.NewModel("viam-viz", "visualization", "world-state-store") + +func init() { + resource.RegisterService( + worldstatestore.API, + Model, + resource.Registration[worldstatestore.Service, *Config]{Constructor: newStore}, + ) +} + +func main() { + ctx := context.Background() + logger := logging.NewDebugLogger("visualization") + + mod, err := module.NewModuleFromArgs(ctx) + if err != nil { + logger.Fatal(err) + } + if err := mod.AddModelFromRegistry(ctx, worldstatestore.API, Model); err != nil { + logger.Fatal(err) + } + if err := mod.Start(ctx); err != nil { + logger.Fatal(err) + } + defer mod.Close(ctx) + + <-ctx.Done() +} diff --git a/modules/visualization/meta.json b/modules/visualization/meta.json new file mode 100644 index 000000000..513aa2db8 --- /dev/null +++ b/modules/visualization/meta.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://dl.viam.dev/module.schema.json", + "module_id": "viam-viz:visualization", + "visibility": "public", + "url": "https://github.com/viam-labs/motion-tools", + "description": "Serves the draw API over the Viam world_state_store API, so lines, points, arrows, NURBS and glTF models render in the visualizer from any environment.", + "models": [ + { + "api": "rdk:service:world_state_store", + "model": "viam-viz:visualization:world-state-store", + "short_description": "Draw geometry and visualization primitives to the visualizer from any Viam SDK.", + "markdown_link": "README.md" + } + ], + "entrypoint": "bin/visualization", + "build": { + "build": "./build.sh", + "path": "bin/visualization", + "arch": ["linux/amd64", "linux/arm64", "darwin/arm64"] + } +} diff --git a/modules/visualization/store.go b/modules/visualization/store.go new file mode 100644 index 000000000..b39758051 --- /dev/null +++ b/modules/visualization/store.go @@ -0,0 +1,364 @@ +package main + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "sync" + + "github.com/google/uuid" + "github.com/viam-labs/motion-tools/draw" + drawv1 "github.com/viam-labs/motion-tools/draw/v1" + "github.com/viam-labs/motion-tools/worldstate" + commonpb "go.viam.com/api/common/v1" + pb "go.viam.com/api/service/worldstatestore/v1" + "go.viam.com/rdk/logging" + "go.viam.com/rdk/resource" + "go.viam.com/rdk/services/worldstatestore" +) + +// streamBufferSize bounds how far a StreamTransformChanges consumer may fall behind. The stream +// is incremental, so dropping a change corrupts it permanently; end the stream instead. +const streamBufferSize = 4096 + +// ErrTransformNotFound is returned when GetTransform names a UUID the world does not hold. +var ErrTransformNotFound = errors.New("no transform with that uuid") + +// Config configures the visualization module. +type Config struct { + // TempDir buffers chunked entity payloads. Never point it at a directory holding anything + // else: the draw service clears stale files there at startup. + TempDir string `json:"temp_dir,omitempty"` +} + +// Validate implements resource.ConfigValidator. Every field has a usable default. +func (conf *Config) Validate(string) ([]string, []string, error) { + return nil, nil, nil +} + +func defaultTempDir() string { + return filepath.Join(os.TempDir(), "viam-visualization") +} + +// projectedEntity is one draw entity as it appears over the world_state_store API. The kind is +// kept because a bulk clear names a scope rather than individual UUIDs. +type projectedEntity struct { + transform *commonpb.Transform + drawing bool +} + +// Store serves the draw API's scene over the world_state_store API. +// +// Writes land in an in-process draw.DrawService; a worker follows its stream, projects each +// Drawing onto a Transform, and publishes the result to StreamTransformChanges. +type Store struct { + resource.Named + resource.TriviallyReconfigurable + + logger logging.Logger + svc *draw.DrawService + commands map[string]commandHandler + + streamCtx context.Context + cancel context.CancelFunc + workers sync.WaitGroup + + mu sync.RWMutex + entities map[string]projectedEntity + + subsMu sync.Mutex + subs map[chan worldstatestore.TransformChange]struct{} +} + +var _ worldstatestore.Service = (*Store)(nil) + +func newStore( + _ context.Context, + _ resource.Dependencies, + conf resource.Config, + logger logging.Logger, +) (worldstatestore.Service, error) { + newConf, err := resource.NativeConfig[*Config](conf) + if err != nil { + return nil, err + } + + tempDir := newConf.TempDir + if tempDir == "" { + tempDir = defaultTempDir() + } + + streamCtx, cancel := context.WithCancel(context.Background()) + + store := &Store{ + Named: conf.ResourceName().AsNamed(), + logger: logger, + svc: draw.NewDrawService(tempDir), + streamCtx: streamCtx, + cancel: cancel, + entities: map[string]projectedEntity{}, + subs: map[chan worldstatestore.TransformChange]struct{}{}, + } + store.commands = store.newCommands() + + store.workers.Add(1) + go func() { + defer store.workers.Done() + store.follow() + }() + + return store, nil +} + +// ListUUIDs returns the UUID of every entity currently in the world. +func (s *Store) ListUUIDs(_ context.Context, _ map[string]any) ([][]byte, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + uuids := make([][]byte, 0, len(s.entities)) + for _, entity := range s.entities { + uuids = append(uuids, entity.transform.GetUuid()) + } + + return uuids, nil +} + +// GetTransform returns one entity, already projected. +func (s *Store) GetTransform(_ context.Context, raw []byte, _ map[string]any) (*commonpb.Transform, error) { + key, err := uuidKey(raw) + if err != nil { + return nil, err + } + + s.mu.RLock() + defer s.mu.RUnlock() + + entity, ok := s.entities[key] + if !ok { + return nil, fmt.Errorf("%w: %s", ErrTransformNotFound, key) + } + + return entity.transform, nil +} + +// StreamTransformChanges follows the world until ctx is cancelled. It must return promptly and +// must not fail: RDK's client consumes a message as soon as the stream opens, to surface errors. +func (s *Store) StreamTransformChanges( + ctx context.Context, + _ map[string]any, +) (*worldstatestore.TransformChangeStream, error) { + // No primer message here; RDK's own service server already sends one. + changes := make(chan worldstatestore.TransformChange, streamBufferSize) + + s.subsMu.Lock() + s.subs[changes] = struct{}{} + s.subsMu.Unlock() + + go func() { + <-ctx.Done() + s.unsubscribe(changes) + }() + + return worldstatestore.NewTransformChangeStreamFromChannel(ctx, changes), nil +} + +// Close stops following the draw service and ends every open stream. +func (s *Store) Close(_ context.Context) error { + s.cancel() + s.workers.Wait() + + s.subsMu.Lock() + for changes := range s.subs { + delete(s.subs, changes) + close(changes) + } + s.subsMu.Unlock() + + return nil +} + +// follow projects the draw service's entity stream onto transforms until the store closes. +func (s *Store) follow() { + for { + subscription := s.svc.SubscribeEntities() + err := s.drain(subscription) + subscription.Close() + + if !errors.Is(err, draw.ErrSubscriberOverflow) { + return + } + + // Draining does no I/O, so falling behind means a bug rather than a slow consumer. + // Drop the projected world and let the new subscription's replay rebuild it. + s.logger.Error("draw entity subscription overflowed; rebuilding the world") + s.clearScope(drawv1.EntityScope_ENTITY_SCOPE_ALL) + } +} + +func (s *Store) drain(subscription *draw.EntitySubscription) error { + for { + msgs, err := subscription.Next(s.streamCtx) + if err != nil { + return err + } + + for _, msg := range msgs { + s.apply(msg) + } + } +} + +// apply folds one draw change into the projected world and publishes it. +func (s *Store) apply(msg *drawv1.StreamEntityChangesResponse) { + if scope := msg.GetClearedScope(); scope != drawv1.EntityScope_ENTITY_SCOPE_UNSPECIFIED { + s.clearScope(scope) + return + } + + transform, isDrawing, err := projectEntity(msg) + if err != nil { + s.logger.Errorw("projecting draw entity", "error", err) + return + } + if transform == nil { + return + } + + key, err := uuidKey(transform.GetUuid()) + if err != nil { + s.logger.Errorw("draw entity has an unusable uuid", "error", err) + return + } + + if msg.GetChangeType() == drawv1.EntityChangeType_ENTITY_CHANGE_TYPE_REMOVED { + s.remove(key) + return + } + + paths := msg.GetUpdatedFields().GetPaths() + if isDrawing { + paths = worldstate.ProjectFieldMask(paths) + } + + s.mu.Lock() + s.entities[key] = projectedEntity{transform: transform, drawing: isDrawing} + s.mu.Unlock() + + s.emit(worldstatestore.TransformChange{ + ChangeType: changeType(msg.GetChangeType()), + Transform: transform, + UpdatedFields: paths, + }) +} + +// clearScope expands a draw bulk removal into one REMOVED per entity. The world_state_store +// API has no bulk signal, so clearing a large scene necessarily fans out. +func (s *Store) clearScope(scope drawv1.EntityScope) { + s.mu.RLock() + keys := make([]string, 0, len(s.entities)) + for key, entity := range s.entities { + if clears(scope, entity.drawing) { + keys = append(keys, key) + } + } + s.mu.RUnlock() + + for _, key := range keys { + s.remove(key) + } +} + +func (s *Store) remove(key string) { + s.mu.Lock() + entity, ok := s.entities[key] + delete(s.entities, key) + s.mu.Unlock() + + if !ok { + return + } + + s.emit(worldstatestore.TransformChange{ + ChangeType: pb.TransformChangeType_TRANSFORM_CHANGE_TYPE_REMOVED, + Transform: &commonpb.Transform{Uuid: entity.transform.GetUuid()}, + }) +} + +func (s *Store) emit(change worldstatestore.TransformChange) { + s.subsMu.Lock() + defer s.subsMu.Unlock() + + for changes := range s.subs { + select { + case changes <- change: + default: + s.logger.Warn("world state subscriber fell behind; closing its stream for resync") + delete(s.subs, changes) + close(changes) + } + } +} + +func (s *Store) unsubscribe(changes chan worldstatestore.TransformChange) { + s.subsMu.Lock() + defer s.subsMu.Unlock() + + if _, ok := s.subs[changes]; !ok { + return + } + delete(s.subs, changes) + close(changes) +} + +// projectEntity renders a stream message's entity as a Transform. Transforms pass through; +// Drawings are projected, since world_state_store carries no Shape of its own. +func projectEntity(msg *drawv1.StreamEntityChangesResponse) (*commonpb.Transform, bool, error) { + switch entity := msg.GetEntity().(type) { + case *drawv1.StreamEntityChangesResponse_Transform: + return entity.Transform, false, nil + case *drawv1.StreamEntityChangesResponse_Drawing: + transform, err := worldstate.ProjectDrawing(entity.Drawing) + return transform, true, err + default: + return nil, false, nil + } +} + +func changeType(source drawv1.EntityChangeType) pb.TransformChangeType { + switch source { + case drawv1.EntityChangeType_ENTITY_CHANGE_TYPE_ADDED: + return pb.TransformChangeType_TRANSFORM_CHANGE_TYPE_ADDED + case drawv1.EntityChangeType_ENTITY_CHANGE_TYPE_UPDATED: + return pb.TransformChangeType_TRANSFORM_CHANGE_TYPE_UPDATED + case drawv1.EntityChangeType_ENTITY_CHANGE_TYPE_REMOVED: + return pb.TransformChangeType_TRANSFORM_CHANGE_TYPE_REMOVED + default: + return pb.TransformChangeType_TRANSFORM_CHANGE_TYPE_UNSPECIFIED + } +} + +func clears(scope drawv1.EntityScope, drawing bool) bool { + switch scope { + case drawv1.EntityScope_ENTITY_SCOPE_ALL: + return true + case drawv1.EntityScope_ENTITY_SCOPE_DRAWINGS: + return drawing + case drawv1.EntityScope_ENTITY_SCOPE_TRANSFORMS: + return !drawing + default: + return false + } +} + +// uuidKey formats raw UUID bytes as the canonical 36-character string, which is what the +// visualizer sends back in get_entity_chunk requests. +func uuidKey(raw []byte) (string, error) { + parsed, err := uuid.FromBytes(raw) + if err != nil { + return "", fmt.Errorf("parsing uuid: %w", err) + } + + return parsed.String(), nil +} diff --git a/modules/visualization/store_e2e_test.go b/modules/visualization/store_e2e_test.go new file mode 100644 index 000000000..d6cea3f1c --- /dev/null +++ b/modules/visualization/store_e2e_test.go @@ -0,0 +1,338 @@ +package main + +import ( + "context" + "os" + "testing" + + "github.com/golang/geo/r3" + "github.com/viam-labs/motion-tools/draw" + drawv1 "github.com/viam-labs/motion-tools/draw/v1" + "github.com/viam-labs/motion-tools/worldstate" + commonv1 "go.viam.com/api/common/v1" + "go.viam.com/rdk/logging" + robotclient "go.viam.com/rdk/robot/client" + "go.viam.com/rdk/services/worldstatestore" + "go.viam.com/rdk/spatialmath" + "go.viam.com/test" + "go.viam.com/utils/rpc" +) + +// Playwright drives these subtests over a live machine one at a time, screenshotting the +// visualizer between each. They skip unless machine credentials are present. +// +// Entities sit in one row along X, 900mm apart, each shape inside a ±250mm envelope. Nothing +// overlaps, so a change to any one of them is unambiguous in a screenshot. +const ( + slotLine = -1800.0 + slotPoints = -900.0 + slotArrows = 0.0 + slotNurbs = 900.0 + slotBox = 1800.0 + + // The chunked cloud gets its own row so filling in never reads as a change to the row above. + slotCloudY = -1200.0 +) + +func getWSClient(t *testing.T) worldstatestore.Service { + t.Helper() + + host := os.Getenv("VIAM_E2E_HOST") + apiKeyID := os.Getenv("VIAM_E2E_API_KEY_ID") + apiKey := os.Getenv("VIAM_E2E_API_KEY") + + if host == "" || apiKeyID == "" || apiKey == "" { + t.Skip("set VIAM_E2E_HOST, VIAM_E2E_API_KEY_ID and VIAM_E2E_API_KEY to run this") + } + + robot, err := robotclient.New( + context.Background(), + host, + logging.NewDebugLogger("visualization-e2e"), + robotclient.WithDialOptions(rpc.WithEntityCredentials( + apiKeyID, + rpc.Credentials{Type: rpc.CredentialsTypeAPIKey, Payload: apiKey}, + )), + ) + test.That(t, err, test.ShouldBeNil) + t.Cleanup(func() { test.That(t, robot.Close(context.Background()), test.ShouldBeNil) }) + + store, err := worldstatestore.FromProvider(robot, "world-state-store") + test.That(t, err, test.ShouldBeNil) + + return store +} + +// sender returns a helper that encodes and runs in one step, so a command builder's two return +// values pass straight through: send(worldstate.AddDrawingCommand(...)). +func sender(t *testing.T, store worldstatestore.Service) func(map[string]any, error) { + return func(cmd map[string]any, err error) { + t.Helper() + + test.That(t, err, test.ShouldBeNil) + + _, err = store.DoCommand(context.Background(), cmd) + test.That(t, err, test.ShouldBeNil) + } +} + +// at places an entity in the world frame. WithID derives the UUID from the name, so a later +// subtest can address it without threading a UUID between separate `go test -run` invocations. +func at(name string, x, y, z float64) []draw.DrawableOption { + return []draw.DrawableOption{ + draw.WithID(name), + draw.WithPose(spatialmath.NewPoseFromPoint(r3.Vector{X: x, Y: y, Z: z})), + } +} + +func namedUUID(name string) []byte { + return draw.NewDrawConfig(name, draw.WithID(name)).UUID +} + +func TestVisualizationLayout(t *testing.T) { + store := getWSClient(t) + + // A shallow V, so a later reshape into a tall spike is obvious at a glance. + t.Run("Line", func(t *testing.T) { + send := sender(t, store) + + line, err := draw.NewLine( + []r3.Vector{{X: -250}, {Z: 200}, {X: 250}}, + draw.WithLineWidth(12), + draw.WithSingleLineColor(draw.NewColor(draw.WithRGB(0, 120, 255))), + ) + test.That(t, err, test.ShouldBeNil) + + send(worldstate.AddDrawingCommand(line.Draw("viz-line", at("viz-line", slotLine, 0, 0)...).ToProto())) + }) + + // A 4x4 grid at 150mm spacing: 450mm square, well inside its slot. + t.Run("Points", func(t *testing.T) { + send := sender(t, store) + + positions := make([]r3.Vector, 0, 16) + for row := range 4 { + for col := range 4 { + positions = append(positions, r3.Vector{X: float64(col*150 - 225), Y: float64(row*150 - 225), Z: 150}) + } + } + + points, err := draw.NewPoints( + positions, + draw.WithPointsSize(60), + draw.WithSinglePointColor(draw.NewColor(draw.WithRGB(0, 220, 160))), + ) + test.That(t, err, test.ShouldBeNil) + + send(worldstate.AddDrawingCommand(points.Draw("viz-points", at("viz-points", slotPoints, 0, 0)...).ToProto())) + }) + + // Three arrows pointing up Z, so hiding them leaves an obvious hole in the row. + t.Run("Arrows", func(t *testing.T) { + send := sender(t, store) + + poses := []spatialmath.Pose{ + spatialmath.NewPoseFromPoint(r3.Vector{X: -200}), + spatialmath.NewPoseFromPoint(r3.Vector{}), + spatialmath.NewPoseFromPoint(r3.Vector{X: 200}), + } + + arrows, err := draw.NewArrows(poses, draw.WithSingleArrowColor(draw.NewColor(draw.WithRGB(255, 180, 0)))) + test.That(t, err, test.ShouldBeNil) + + send(worldstate.AddDrawingCommand(arrows.Draw("viz-arrows", at("viz-arrows", slotArrows, 0, 0)...).ToProto())) + }) + + // A clamped cubic arc: 4 control points needs len(controlPoints)+degree+1 = 8 knots. + t.Run("Nurbs", func(t *testing.T) { + send := sender(t, store) + + controlPoints := []spatialmath.Pose{ + spatialmath.NewPoseFromPoint(r3.Vector{X: -250}), + spatialmath.NewPoseFromPoint(r3.Vector{X: -100, Z: 400}), + spatialmath.NewPoseFromPoint(r3.Vector{X: 100, Z: 400}), + spatialmath.NewPoseFromPoint(r3.Vector{X: 250}), + } + + nurbs, err := draw.NewNurbs( + controlPoints, + []float64{0, 0, 0, 0, 1, 1, 1, 1}, + draw.WithNurbsDegree(3), + draw.WithNurbsLineWidth(12), + draw.WithNurbsColors(draw.NewColor(draw.WithRGB(200, 80, 255))), + ) + test.That(t, err, test.ShouldBeNil) + + send(worldstate.AddDrawingCommand(nurbs.Draw("viz-nurbs", at("viz-nurbs", slotNurbs, 0, 0)...).ToProto())) + }) + + // A native Transform, to prove projected drawings and real geometry share one world. + t.Run("Box", func(t *testing.T) { + send := sender(t, store) + + box, err := spatialmath.NewBox(spatialmath.NewZeroPose(), r3.Vector{X: 300, Y: 300, Z: 300}, "viz-box") + test.That(t, err, test.ShouldBeNil) + + drawn, err := draw.NewDrawnGeometry(box, draw.WithGeometryColor(draw.NewColor(draw.WithRGB(255, 90, 90)))) + test.That(t, err, test.ShouldBeNil) + + transform, err := drawn.Draw("viz-box", at("viz-box", slotBox, 0, 150)...) + test.That(t, err, test.ShouldBeNil) + + send(worldstate.AddTransformCommand(transform)) + }) +} + +// Each update changes one entity in one unmistakable way, so a screenshot diff points at exactly +// one thing. +func TestVisualizationUpdate(t *testing.T) { + store := getWSClient(t) + + // The V becomes a tall spike, well above anything else in the row. + t.Run("ReshapeLine", func(t *testing.T) { + send := sender(t, store) + + line, err := draw.NewLine( + []r3.Vector{{X: -250}, {Z: 900}, {X: 250}}, + draw.WithLineWidth(12), + draw.WithSingleLineColor(draw.NewColor(draw.WithRGB(0, 120, 255))), + ) + test.That(t, err, test.ShouldBeNil) + + drawing := line.Draw("viz-line", at("viz-line", slotLine, 0, 0)...).ToProto() + send(worldstate.UpdateDrawingCommand(namedUUID("viz-line"), drawing, draw.DrawingPathShape)) + }) + + // Green to magenta: a hue jump, not a shade of the same colour. + t.Run("RecolorPoints", func(t *testing.T) { + send := sender(t, store) + + recoloured := &drawv1.Drawing{ + ReferenceFrame: "viz-points", + Metadata: &drawv1.Metadata{Colors: []byte{255, 0, 200}, ColorFormat: drawv1.ColorFormat_COLOR_FORMAT_RGB}, + } + + send(worldstate.UpdateDrawingCommand(namedUUID("viz-points"), recoloured, draw.DrawingPathMetadataColors)) + }) + + // The arrows disappear, leaving a hole in the middle of the row. + t.Run("HideArrows", func(t *testing.T) { + send := sender(t, store) + + hidden := &drawv1.Drawing{ + ReferenceFrame: "viz-arrows", + Metadata: &drawv1.Metadata{Invisible: ptr(true)}, + } + + send(worldstate.UpdateDrawingCommand(namedUUID("viz-arrows"), hidden, draw.DrawingPathMetadataInvisible)) + }) + + t.Run("ShowArrows", func(t *testing.T) { + send := sender(t, store) + + shown := &drawv1.Drawing{ + ReferenceFrame: "viz-arrows", + Metadata: &drawv1.Metadata{Invisible: ptr(false)}, + } + + send(worldstate.UpdateDrawingCommand(namedUUID("viz-arrows"), shown, draw.DrawingPathMetadataInvisible)) + }) + + // A pose change on a native Transform: the box lifts a metre clear of the row. + t.Run("RaiseBox", func(t *testing.T) { + send := sender(t, store) + + raised := &commonv1.Transform{ + ReferenceFrame: "viz-box", + PoseInObserverFrame: &commonv1.PoseInFrame{ + ReferenceFrame: "world", + Pose: &commonv1.Pose{X: slotBox, Y: 0, Z: 1200, OZ: 1}, + }, + } + + send(worldstate.UpdateTransformCommand(namedUUID("viz-box"), raised, draw.TransformPathPoseValue)) + }) +} + +// A 2,000-point cloud delivered 200 at a time, so the visualizer has to pull the remainder back +// with get_entity_chunk. It grows visibly along +X as the chunks land. +func TestVisualizationChunkedCloud(t *testing.T) { + store := getWSClient(t) + + const total, chunkSize = 2000, 200 + + positions := make([]r3.Vector, 0, total) + for i := range total { + positions = append(positions, r3.Vector{ + X: float64(i)*0.9 - 900, + Y: slotCloudY, + Z: float64(200 + 150*((i/50)%3)), + }) + } + + chunkOf := func(start int) *drawv1.Drawing { + points, err := draw.NewPoints( + positions[start:start+chunkSize], + draw.WithPointsSize(40), + draw.WithSinglePointColor(draw.NewColor(draw.WithRGB(80, 200, 255))), + ) + test.That(t, err, test.ShouldBeNil) + + return points.Draw("viz-cloud", at("viz-cloud", 0, 0, 0)...).ToProto() + } + + t.Run("Seed", func(t *testing.T) { + send := sender(t, store) + + seed := chunkOf(0) + seed.Metadata.Chunks = &drawv1.Chunks{ChunkSize: chunkSize, Total: total, Stride: 12} + + send(worldstate.AddDrawingCommand(seed)) + }) + + // Unmasked updates are chunk appends rather than field patches, and emit no stream traffic; + // the visualizer pulls them itself. + t.Run("Fill", func(t *testing.T) { + send := sender(t, store) + + for start := chunkSize; start < total; start += chunkSize { + send(worldstate.UpdateDrawingCommand(namedUUID("viz-cloud"), chunkOf(start))) + } + }) +} + +// TestVisualizationReset clears the scene so a run starts from a known-empty world. +// +// The module holds its entities in process and an unchanged machine config does not restart it, +// so a run that ended before TestVisualizationRemove leaves them for the next one to photograph. +func TestVisualizationReset(t *testing.T) { + sender(t, getWSClient(t))(worldstate.RemoveAllCommand(), nil) +} + +// Removal is one entity at a time so each screenshot shows exactly one thing gone, then a bulk +// clear which the world_state_store API has to fan out into one removal per entity. +func TestVisualizationRemove(t *testing.T) { + store := getWSClient(t) + + t.Run("RemoveLine", func(t *testing.T) { + send := sender(t, store) + + send(worldstate.RemoveEntityCommand(namedUUID("viz-line"))) + }) + + t.Run("RemoveBox", func(t *testing.T) { + send := sender(t, store) + + send(worldstate.RemoveEntityCommand(namedUUID("viz-box"))) + }) + + t.Run("RemoveAll", func(t *testing.T) { + send := sender(t, store) + + send(worldstate.RemoveAllCommand(), nil) + }) +} + +func ptr[T any](value T) *T { + return &value +} diff --git a/modules/visualization/store_test.go b/modules/visualization/store_test.go new file mode 100644 index 000000000..c0f9ddb90 --- /dev/null +++ b/modules/visualization/store_test.go @@ -0,0 +1,414 @@ +package main + +import ( + "context" + "encoding/base64" + "encoding/binary" + "errors" + "io" + "math" + "testing" + "time" + + "github.com/golang/geo/r3" + "github.com/viam-labs/motion-tools/draw" + "github.com/viam-labs/motion-tools/worldstate" + pb "go.viam.com/api/service/worldstatestore/v1" + "go.viam.com/rdk/logging" + "go.viam.com/rdk/resource" + "go.viam.com/rdk/services/worldstatestore" + "go.viam.com/rdk/spatialmath" + "go.viam.com/test" + "google.golang.org/protobuf/proto" +) + +func newTestStore(t *testing.T) *Store { + t.Helper() + + conf := resource.Config{ + Name: "world-state-store", + API: worldstatestore.API, + Model: Model, + ConvertedAttributes: &Config{TempDir: t.TempDir()}, + } + + svc, err := newStore(context.Background(), nil, conf, logging.NewTestLogger(t)) + test.That(t, err, test.ShouldBeNil) + t.Cleanup(func() { test.That(t, svc.Close(context.Background()), test.ShouldBeNil) }) + + store, ok := svc.(*Store) + test.That(t, ok, test.ShouldBeTrue) + + return store +} + +// float32sToBytes packs floats the way every draw shape buffer is encoded: little-endian +// float32, three per position. +func float32sToBytes(values []float32) []byte { + raw := make([]byte, len(values)*4) + for i, v := range values { + binary.LittleEndian.PutUint32(raw[i*4:], math.Float32bits(v)) + } + + return raw +} + +// encodeFloat32s packs floats and base64s them, which is how a bytes field travels in +// protojson. +func encodeFloat32s(values []float32) string { + return base64.StdEncoding.EncodeToString(float32sToBytes(values)) +} + +// waitFor polls until condition holds, so tests never race the follower goroutine. +func waitFor(t *testing.T, condition func() bool) { + t.Helper() + + deadline := time.After(5 * time.Second) + for { + if condition() { + return + } + select { + case <-deadline: + t.Fatal("condition not met within timeout") + case <-time.After(5 * time.Millisecond): + } + } +} + +func addLine(t *testing.T, store *Store, name string, positions []byte) []byte { + t.Helper() + + response, err := store.DoCommand(context.Background(), map[string]any{ + "command": "AddEntity", + "request": map[string]any{ + "drawing": map[string]any{ + "reference_frame": name, + "pose_in_observer_frame": map[string]any{ + "reference_frame": "world", + "pose": map[string]any{"x": 1.0, "oz": 1.0}, + }, + "physical_object": map[string]any{ + "line": map[string]any{ + "positions": base64.StdEncoding.EncodeToString(positions), + "line_width": 7.0, + }, + }, + "metadata": map[string]any{ + "colors": base64.StdEncoding.EncodeToString([]byte{1, 2, 3}), + }, + }, + }, + }) + test.That(t, err, test.ShouldBeNil) + + encoded, ok := response["uuid"].(string) + test.That(t, ok, test.ShouldBeTrue) + + raw, err := base64.StdEncoding.DecodeString(encoded) + test.That(t, err, test.ShouldBeNil) + + return raw +} + +func TestDoCommandTunnelsADrawingAndProjectsIt(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + positions := []byte{0, 0, 128, 63} + raw := addLine(t, store, "my-line", positions) + + waitFor(t, func() bool { + uuids, err := store.ListUUIDs(ctx, nil) + return err == nil && len(uuids) == 1 + }) + + transform, err := store.GetTransform(ctx, raw, nil) + test.That(t, err, test.ShouldBeNil) + test.That(t, transform.GetReferenceFrame(), test.ShouldEqual, "my-line") + test.That(t, transform.GetPoseInObserverFrame().GetReferenceFrame(), test.ShouldEqual, "world") + + // A Line has no common.v1.Geometry equivalent, so it travels in the metadata Struct. + test.That(t, transform.GetPhysicalObject(), test.ShouldBeNil) + + shape, err := worldstate.ProjectedShape(transform.GetMetadata()) + test.That(t, err, test.ShouldBeNil) + test.That(t, shape.GetLine().GetLineWidth(), test.ShouldEqual, float32(7)) + test.That(t, shape.GetLine().GetPositions(), test.ShouldResemble, positions) +} + +// The path a user of this module actually takes: build with draw's shape builders, encode with +// worldstate.Command, send through DoCommand. Nobody should be hand-writing protojson. +func TestBuiltDrawingsSurviveTheCommandPath(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + line, err := draw.NewLine( + []r3.Vector{{X: -250}, {Z: 200}, {X: 250}}, + draw.WithLineWidth(12), + draw.WithSingleLineColor(draw.NewColor(draw.WithRGB(0, 120, 255))), + ) + test.That(t, err, test.ShouldBeNil) + + built := line.Draw("built-line", + draw.WithID("built-line"), + draw.WithPose(spatialmath.NewPoseFromPoint(r3.Vector{X: 1000, Z: 250})), + ).ToProto() + + cmd, err := worldstate.AddDrawingCommand(built) + test.That(t, err, test.ShouldBeNil) + + _, err = store.DoCommand(ctx, cmd) + test.That(t, err, test.ShouldBeNil) + + waitFor(t, func() bool { + uuids, listErr := store.ListUUIDs(ctx, nil) + return listErr == nil && len(uuids) == 1 + }) + + transform, err := store.GetTransform(ctx, built.GetUuid(), nil) + test.That(t, err, test.ShouldBeNil) + test.That(t, transform.GetReferenceFrame(), test.ShouldEqual, "built-line") + test.That(t, transform.GetPoseInObserverFrame().GetPose().GetX(), test.ShouldEqual, 1000.0) + + // The shape the builder produced has to come back byte-identical on the far side. + shape, err := worldstate.ProjectedShape(transform.GetMetadata()) + test.That(t, err, test.ShouldBeNil) + test.That(t, proto.Equal(shape, built.GetPhysicalObject()), test.ShouldBeTrue) + + // And a builder-made update lands on the same entity, addressed by its derived UUID. + raised, err := draw.NewLine([]r3.Vector{{X: -250}, {Z: 900}, {X: 250}}, draw.WithLineWidth(12)) + test.That(t, err, test.ShouldBeNil) + + update := raised.Draw("built-line", draw.WithID("built-line")).ToProto() + cmd, err = worldstate.UpdateDrawingCommand(built.GetUuid(), update, draw.DrawingPathShape) + test.That(t, err, test.ShouldBeNil) + + _, err = store.DoCommand(ctx, cmd) + test.That(t, err, test.ShouldBeNil) + + waitFor(t, func() bool { + current, getErr := store.GetTransform(ctx, built.GetUuid(), nil) + if getErr != nil { + return false + } + updated, shapeErr := worldstate.ProjectedShape(current.GetMetadata()) + return shapeErr == nil && proto.Equal(updated, update.GetPhysicalObject()) + }) +} + +func TestTransformsPassThroughWithoutAShape(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + _, err := store.DoCommand(ctx, map[string]any{ + "command": "AddEntity", + "request": map[string]any{ + "transform": map[string]any{ + "reference_frame": "my-box", + "pose_in_observer_frame": map[string]any{"reference_frame": "world"}, + "physical_object": map[string]any{ + "box": map[string]any{"dims_mm": map[string]any{"x": 100.0, "y": 100.0, "z": 100.0}}, + }, + }, + }, + }) + test.That(t, err, test.ShouldBeNil) + + var uuids [][]byte + waitFor(t, func() bool { + listed, listErr := store.ListUUIDs(ctx, nil) + uuids = listed + return listErr == nil && len(listed) == 1 + }) + + transform, err := store.GetTransform(ctx, uuids[0], nil) + test.That(t, err, test.ShouldBeNil) + test.That(t, transform.GetPhysicalObject().GetBox().GetDimsMm().GetX(), test.ShouldEqual, 100.0) + + shape, err := worldstate.ProjectedShape(transform.GetMetadata()) + test.That(t, err, test.ShouldBeNil) + test.That(t, shape, test.ShouldBeNil) +} + +func TestStreamDeliversProjectedChanges(t *testing.T) { + store := newTestStore(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + stream, err := store.StreamTransformChanges(ctx, nil) + test.That(t, err, test.ShouldBeNil) + + addLine(t, store, "streamed-line", []byte{0, 0, 128, 63}) + + change, err := stream.Next() + test.That(t, err, test.ShouldBeNil) + test.That(t, change.ChangeType, test.ShouldEqual, pb.TransformChangeType_TRANSFORM_CHANGE_TYPE_ADDED) + test.That(t, change.Transform.GetReferenceFrame(), test.ShouldEqual, "streamed-line") + + shape, err := worldstate.ProjectedShape(change.Transform.GetMetadata()) + test.That(t, err, test.ShouldBeNil) + test.That(t, shape.GetLine(), test.ShouldNotBeNil) +} + +func TestRemoveAllFansOutOneRemovedPerEntity(t *testing.T) { + store := newTestStore(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + addLine(t, store, "first", []byte{0, 0, 128, 63}) + addLine(t, store, "second", []byte{0, 0, 0, 64}) + + waitFor(t, func() bool { + uuids, err := store.ListUUIDs(ctx, nil) + return err == nil && len(uuids) == 2 + }) + + stream, err := store.StreamTransformChanges(ctx, nil) + test.That(t, err, test.ShouldBeNil) + + _, err = store.DoCommand(ctx, map[string]any{"command": "RemoveAll"}) + test.That(t, err, test.ShouldBeNil) + + // The world_state_store API has no bulk-removal signal, so the scope becomes one event each. + for range 2 { + change, nextErr := stream.Next() + test.That(t, nextErr, test.ShouldBeNil) + test.That(t, change.ChangeType, test.ShouldEqual, pb.TransformChangeType_TRANSFORM_CHANGE_TYPE_REMOVED) + } + + waitFor(t, func() bool { + uuids, listErr := store.ListUUIDs(ctx, nil) + return listErr == nil && len(uuids) == 0 + }) +} + +// A chunked drawing arrives as an AddEntity declaring the total, then one unmasked UpdateEntity +// per remaining chunk. This walks that loop and pulls it back with get_entity_chunk. +func TestChunkedPointCloudRoundTrip(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + const total, chunkSize, stride = 40, 10, 12 + + positions := make([]float32, total*3) + for i := range total { + positions[i*3] = float32(i * 10) + positions[i*3+1] = float32(i * 5) + positions[i*3+2] = 100 + } + + response, err := store.DoCommand(ctx, map[string]any{ + "command": "AddEntity", + "request": map[string]any{ + "drawing": map[string]any{ + "reference_frame": "cloud", + "pose_in_observer_frame": map[string]any{"reference_frame": "world"}, + "physical_object": map[string]any{ + "points": map[string]any{"positions": encodeFloat32s(positions[:chunkSize*3])}, + }, + "metadata": map[string]any{ + "chunks": map[string]any{"chunk_size": chunkSize, "total": total, "stride": stride}, + }, + }, + }, + }) + test.That(t, err, test.ShouldBeNil) + + encoded, ok := response["uuid"].(string) + test.That(t, ok, test.ShouldBeTrue) + raw, err := base64.StdEncoding.DecodeString(encoded) + test.That(t, err, test.ShouldBeNil) + + for start := chunkSize; start < total; start += chunkSize { + _, err = store.DoCommand(ctx, map[string]any{ + "command": "UpdateEntity", + "request": map[string]any{ + "uuid": encoded, + "drawing": map[string]any{ + // An append is still validated as an update, so it has to name the same frame. + "reference_frame": "cloud", + "physical_object": map[string]any{ + "points": map[string]any{ + "positions": encodeFloat32s(positions[start*3 : (start+chunkSize)*3]), + }, + }, + }, + }, + }) + test.That(t, err, test.ShouldBeNil) + } + + // The projected transform advertises the chunking so the visualizer knows to pull. + waitFor(t, func() bool { + uuids, listErr := store.ListUUIDs(ctx, nil) + return listErr == nil && len(uuids) == 1 + }) + + transform, err := store.GetTransform(ctx, raw, nil) + test.That(t, err, test.ShouldBeNil) + + chunks := transform.GetMetadata().GetFields()["chunks"].GetStructValue().GetFields() + test.That(t, chunks["total"].GetNumberValue(), test.ShouldEqual, float64(total)) + test.That(t, chunks["chunk_size"].GetNumberValue(), test.ShouldEqual, float64(chunkSize)) + + uuidString, err := uuidKey(raw) + test.That(t, err, test.ShouldBeNil) + + // The chunk reply keeps the shape the visualizer's decoder already reads. + chunk, err := store.DoCommand(ctx, map[string]any{ + "command": "get_entity_chunk", + "uuid": uuidString, + "start": float64(chunkSize), + }) + test.That(t, err, test.ShouldBeNil) + test.That(t, chunk["start"], test.ShouldEqual, float64(chunkSize)) + + entity, ok := chunk["entity"].(map[string]any) + test.That(t, ok, test.ShouldBeTrue) + + physical, ok := entity["physical_object"].(map[string]any) + test.That(t, ok, test.ShouldBeTrue) + points, ok := physical["points"].(map[string]any) + test.That(t, ok, test.ShouldBeTrue) + + got, err := base64.StdEncoding.DecodeString(points["positions"].(string)) + test.That(t, err, test.ShouldBeNil) + test.That(t, got, test.ShouldResemble, float32sToBytes(positions[chunkSize*3:(chunkSize*2)*3])) +} + +func TestDoCommandRejectsUnusableCommands(t *testing.T) { + store := newTestStore(t) + ctx := context.Background() + + _, err := store.DoCommand(ctx, map[string]any{}) + test.That(t, err, test.ShouldBeError, ErrMissingCommand) + + _, err = store.DoCommand(ctx, map[string]any{"command": "Nonexistent"}) + test.That(t, err, test.ShouldNotBeNil) + test.That(t, err.Error(), test.ShouldContainSubstring, "unknown command") + + _, err = store.DoCommand(ctx, map[string]any{"command": "AddEntity", "request": "not-an-object"}) + test.That(t, err, test.ShouldNotBeNil) +} + +func TestGetTransformReportsMissingEntities(t *testing.T) { + store := newTestStore(t) + + _, err := store.GetTransform(context.Background(), make([]byte, 16), nil) + test.That(t, errors.Is(err, ErrTransformNotFound), test.ShouldBeTrue) +} + +func TestClosedStreamEnds(t *testing.T) { + store := newTestStore(t) + ctx, cancel := context.WithCancel(context.Background()) + + stream, err := store.StreamTransformChanges(ctx, nil) + test.That(t, err, test.ShouldBeNil) + + cancel() + + _, err = stream.Next() + test.That(t, err, test.ShouldNotBeNil) + test.That(t, errors.Is(err, context.Canceled) || errors.Is(err, io.EOF), test.ShouldBeTrue) +} diff --git a/modules/visualization/visualization b/modules/visualization/visualization new file mode 100755 index 000000000..3410f4faa Binary files /dev/null and b/modules/visualization/visualization differ diff --git a/package.json b/package.json index e2ed92370..4b9d876c7 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "vite-build": "wireit", "build:workers": "wireit", "go-build": "wireit", + "visualization-build": "wireit", "package": "wireit", "svelte-sync": "wireit", "prepare": "pnpm svelte-sync && pnpm build:workers || echo ''", @@ -26,6 +27,8 @@ "lint:eslint": "wireit", "lint:draw": "wireit", "lint:client": "wireit", + "lint:worldstate": "wireit", + "lint:visualization": "wireit", "knip": "npx knip --include files,exports,types", "test": "wireit", "test:unit": "vitest", @@ -33,12 +36,16 @@ "test:coverage": "wireit", "test:client": "wireit", "test:draw": "wireit", + "test:worldstate": "wireit", + "test:visualization": "wireit", "test:e2e": "./e2e/setup.sh && playwright test", "test:e2e-ui": "./e2e/setup.sh && playwright test --ui", "test:e2e-ci": "./e2e/setup.sh && playwright test e2e/edit-frame.test.ts e2e/file-drop.test.ts --config playwright.ci.config.ts", "vet": "wireit", "vet:draw": "wireit", "vet:client": "wireit", + "vet:worldstate": "wireit", + "vet:visualization": "wireit", "proto": "wireit", "proto:vendor": "wireit", "proto:dep-update": "buf dep update", @@ -129,6 +136,21 @@ ".bin/draw-server" ] }, + "visualization-build": { + "command": "cd modules/visualization && ./build.sh", + "files": [ + "modules/visualization/**", + "!modules/visualization/bin/**", + "draw/**", + "worldstate/**", + "go.mod", + "go.sum", + "modules/visualization/build.sh" + ], + "output": [ + "modules/visualization/bin/visualization" + ] + }, "package": { "command": "svelte-package && publint", "files": [ @@ -196,7 +218,9 @@ "dependencies": [ "lint:prettier", "lint:eslint", - "lint:draw" + "lint:draw", + "lint:worldstate", + "lint:visualization" ] }, "lint:prettier": { @@ -268,7 +292,7 @@ ] }, "lint:draw": { - "command": "golangci-lint run ./draw/...", + "command": "golangci-lint run --allow-parallel-runners ./draw/...", "files": [ "draw/**", "go.mod", @@ -278,7 +302,7 @@ "output": [] }, "lint:client": { - "command": "golangci-lint run ./client/...", + "command": "golangci-lint run --allow-parallel-runners ./client/...", "files": [ "client/**", "go.mod", @@ -287,10 +311,36 @@ ], "output": [] }, + "lint:worldstate": { + "command": "golangci-lint run --allow-parallel-runners ./worldstate/...", + "files": [ + "worldstate/**", + "draw/**", + "go.mod", + "go.sum", + ".golangci.yml" + ], + "output": [] + }, + "lint:visualization": { + "command": "cd modules/visualization && golangci-lint run --allow-parallel-runners ./...", + "files": [ + "modules/visualization/**", + "!modules/visualization/bin/**", + "draw/**", + "worldstate/**", + "go.mod", + "go.sum", + ".golangci.yml" + ], + "output": [] + }, "vet": { "dependencies": [ "vet:draw", - "vet:client" + "vet:client", + "vet:worldstate", + "vet:visualization" ] }, "vet:draw": { @@ -311,6 +361,28 @@ ], "output": [] }, + "vet:worldstate": { + "command": "go vet ./worldstate/...", + "files": [ + "worldstate/**", + "draw/**", + "go.mod", + "go.sum" + ], + "output": [] + }, + "vet:visualization": { + "command": "cd modules/visualization && go vet ./...", + "files": [ + "modules/visualization/**", + "!modules/visualization/bin/**", + "draw/**", + "worldstate/**", + "go.mod", + "go.sum" + ], + "output": [] + }, "test": { "dependencies": [ "test:unit-run" @@ -368,6 +440,28 @@ ], "output": [] }, + "test:worldstate": { + "command": "go test ./worldstate/... -count=1", + "files": [ + "worldstate/**", + "draw/**", + "go.mod", + "go.sum" + ], + "output": [] + }, + "test:visualization": { + "command": "cd modules/visualization && go test ./... -count=1", + "files": [ + "modules/visualization/**", + "!modules/visualization/bin/**", + "draw/**", + "worldstate/**", + "go.mod", + "go.sum" + ], + "output": [] + }, "proto": { "command": "pnpm proto:dep-update && pnpm proto:format && pnpm proto:lint && pnpm proto:gen-go && pnpm proto:gen-ts", "dependencies": [ diff --git a/protos/draw/v1/worldstate.proto b/protos/draw/v1/worldstate.proto new file mode 100644 index 000000000..427cca63b --- /dev/null +++ b/protos/draw/v1/worldstate.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package draw.v1; + +import "draw/v1/drawing.proto"; + +option go_package = "github.com/viam-labs/motion-tools/draw/v1;drawv1"; + +// DrawingProjection is merged into a viam.common.v1.Transform's metadata when a Drawing +// travels over the world_state_store API. +// +// That API carries only common.v1.Geometry, so a Shape has no native home and rides in the +// metadata Struct instead, next to draw.v1.Metadata's fields. +// +// Both halves are encoded with protojson using proto field names and numeric enums. +message DrawingProjection { + // The shape this transform really is. Present only on projected Drawings; a Transform + // that carries no shape is a true Transform and renders from its physical_object. + Shape shape = 1; +} diff --git a/src/lib/__tests__/draw.spec.ts b/src/lib/__tests__/draw.spec.ts index e185ec529..84726791e 100644 --- a/src/lib/__tests__/draw.spec.ts +++ b/src/lib/__tests__/draw.spec.ts @@ -1,3 +1,6 @@ +import type { JsonObject } from '@bufbuild/protobuf' + +import { Struct } from '@viamrobotics/sdk' import { createWorld, type World } from 'koota' import { afterEach, describe, expect, it, vi } from 'vitest' @@ -21,12 +24,20 @@ import { Shape, } from '$lib/buf/draw/v1/drawing_pb' import { ColorFormat, Metadata, Relationship } from '$lib/buf/draw/v1/metadata_pb' +import { DrawingProjection } from '$lib/buf/draw/v1/worldstate_pb' import { STRIDE } from '$lib/buffer' import { createChunkLoader, type EntityChunk } from '$lib/chunking' import { traits } from '$lib/ecs' import { Pose } from '$lib/math' -import { drawDrawing, drawTransform, updateDrawing, updateMetadata, updateTransform } from '../draw' +import { + drawDrawing, + drawTransform, + shapeFromStruct, + updateDrawing, + updateMetadata, + updateTransform, +} from '../draw' /** Packs a flat list of float32 pose components into the proto's little-endian byte layout. */ const packPoses = (...values: number[]): Uint8Array => { @@ -740,6 +751,61 @@ describe('createChunkLoader', () => { expect(geometry.drawRange.count).toBe(total) }) + // A chunked entity that respawns mid-upload calls start again while the previous pull is + // still parked on a fetch. The replacement has to take over, not be dropped as a duplicate. + it('a restart supersedes an in-flight pull', async () => { + world = createWorld() + const total = 6 + const firstChunkEnd = 3 + + const newGeometry = () => + preAllocateBufferGeometry(total, STRIDE.POSITIONS, { colorFormat: ColorFormat.UNSPECIFIED }) + + const chunk: EntityChunk = { + start: firstChunkEnd, + positions: new Float32Array([1, 2, 3, 4, 5, 6, 7, 8, 9]), + done: true, + } + + let calls = 0 + let releaseStale: ((chunk: EntityChunk) => void) | undefined + const fetchChunk = vi.fn(async () => { + calls += 1 + if (calls > 1) return chunk + return await new Promise((resolve) => { + releaseStale = resolve + }) + }) + + const loader = createChunkLoader({ world, invalidate: () => {}, fetchChunk }) + + const metadata: MetadataType = { + colorFormat: ColorFormat.UNSPECIFIED, + chunks: { chunkSize: firstChunkEnd, total, stride: STRIDE.POSITIONS }, + } + + const stale = world.spawn(traits.BufferGeometry(newGeometry())) + loader.start('uuid', stale, metadata) + await vi.waitFor(() => expect(fetchChunk).toHaveBeenCalledTimes(1)) + + stale.destroy() + + const geometry = newGeometry() + const fresh = world.spawn(traits.BufferGeometry(geometry)) + loader.start('uuid', fresh, metadata) + + await vi.waitFor(() => { + expect(fresh.has(traits.ChunkProgress)).toBe(false) + }) + expect(fetchChunk).toHaveBeenCalledTimes(2) + expect(geometry.drawRange.count).toBe(total) + + // The superseded pull resolving afterwards owns nothing, so it must not undo any of that. + releaseStale?.(chunk) + await vi.waitFor(() => expect(fetchChunk).toHaveBeenCalledTimes(2)) + expect(geometry.drawRange.count).toBe(total) + }) + it('dispose aborts in-flight fetches and stops the loop', async () => { world = createWorld() const total = 12 @@ -778,3 +844,124 @@ describe('createChunkLoader', () => { }) }) }) + +/** + * Mirrors the module's projection: a Shape encoded as protojson under the `shape` key of a + * Transform's metadata Struct, alongside the metadata's own fields. + */ +const projectedMetadata = (shape: Shape, extra: JsonObject = {}) => + Struct.fromJson({ + ...extra, + ...(new DrawingProjection({ shape }).toJson() as JsonObject), + }) + +describe('shapeFromStruct', () => { + it.each([ + [ + 'line', + new Shape({ + label: 'a line', + geometryType: { + case: 'line', + value: { positions: new Uint8Array([1, 2, 3, 4]), lineWidth: 7 }, + }, + }), + ], + [ + 'points', + new Shape({ + geometryType: { + case: 'points', + value: { positions: new Uint8Array([5, 6, 7, 8]), pointSize: 11 }, + }, + }), + ], + [ + 'arrows', + new Shape({ geometryType: { case: 'arrows', value: { poses: new Uint8Array([9, 8]) } } }), + ], + [ + 'nurbs', + new Shape({ + geometryType: { + case: 'nurbs', + value: { controlPoints: new Uint8Array([1]), knots: new Uint8Array([2]), degree: 3 }, + }, + }), + ], + [ + 'model', + new Shape({ + geometryType: { + case: 'model', + value: { + assets: [ + { + mimeType: 'model/gltf-binary', + content: { case: 'url', value: 'https://example.com/a.glb' }, + }, + ], + scale: { x: 1, y: 2, z: 3 }, + }, + }, + }), + ], + ])('round-trips a %s shape', (_name, shape) => { + const decoded = shapeFromStruct(projectedMetadata(shape).fields) + + expect(decoded).toBeDefined() + expect(decoded!.equals(shape)).toBe(true) + }) + + it('reads a shape carried alongside metadata fields', () => { + const shape = new Shape({ + geometryType: { case: 'line', value: { positions: new Uint8Array([1, 2, 3, 4]) } }, + }) + + const decoded = shapeFromStruct( + projectedMetadata(shape, { + colors: 'AQID', + color_format: 1, + show_axes_helper: true, + }).fields + ) + + expect(decoded?.geometryType.case).toBe('line') + }) + + it('returns undefined for a real transform', () => { + const metadata = Struct.fromJson({ colors: 'AQID', invisible: false }) + + expect(shapeFromStruct(metadata.fields)).toBeUndefined() + expect(shapeFromStruct()).toBeUndefined() + }) + + it('ignores fields a newer producer added', () => { + const metadata = Struct.fromJson({ + shape: { line: { positions: 'AQIDBA==' }, somethingNewer: 42 }, + }) + + expect(shapeFromStruct(metadata.fields)?.geometryType.case).toBe('line') + }) + + it('drops a malformed shape rather than failing the entity', () => { + const error = vi.spyOn(console, 'error').mockImplementation(() => undefined) + const metadata = Struct.fromJson({ shape: 'not-a-shape' }) + + expect(shapeFromStruct(metadata.fields)).toBeUndefined() + expect(error).toHaveBeenCalled() + }) + + it('accepts what a Drawing projects, so both paths render the same shape', () => { + const drawing = new Drawing({ + referenceFrame: 'my-line', + physicalObject: new Shape({ + geometryType: { case: 'line', value: { positions: new Uint8Array([1, 2, 3, 4]) } }, + }), + }) + + const decoded = shapeFromStruct(projectedMetadata(drawing.physicalObject!).fields) + + expect(decoded!.equals(drawing.physicalObject!)).toBe(true) + }) +}) diff --git a/src/lib/chunking.ts b/src/lib/chunking.ts index 600467fc5..88eaf79fa 100644 --- a/src/lib/chunking.ts +++ b/src/lib/chunking.ts @@ -40,20 +40,27 @@ export const createChunkLoader = ({ fetchChunk, colorFormat = ColorFormat.RGB, }: ChunkLoaderOptions) => { - const active = new Set() + // The pull that owns each uuid. A pull is superseded rather than rejected: an entity that + // respawns mid-upload needs its replacement to take over, and the loop below cannot notice + // its own entity was destroyed until the fetch it is parked on resolves. + const active = new Map() + let lastToken = 0 + const controller = new AbortController() const pull = async (uuid: string, entity: Entity, total: number, firstChunkEnd: number) => { - if (active.has(uuid)) return - active.add(uuid) + const token = ++lastToken + active.set(uuid, token) const { signal } = controller + const owns = () => !signal.aborted && active.get(uuid) === token + let nextStart = firstChunkEnd try { - while (!signal.aborted) { + while (owns()) { const chunk = await fetchChunk(uuid, nextStart, signal) - if (signal.aborted || !chunk) break + if (!owns() || !chunk) break const buffer = entity.get(traits.BufferGeometry) if (!buffer) break @@ -72,24 +79,36 @@ export const createChunkLoader = ({ if (chunk.done) break } } catch (error) { - if (!signal.aborted) { + if (owns()) { console.error(`Chunk pull failed for entity ${uuid}:`, error) } } finally { - active.delete(uuid) - if (world.has(entity)) { - entity.remove(traits.ChunkProgress) + // A superseded pull cleans up nothing: both the map entry and the ChunkProgress trait + // now belong to the pull that replaced it. + if (active.get(uuid) === token) { + active.delete(uuid) + if (world.has(entity)) { + entity.remove(traits.ChunkProgress) + } } } } return { - start(uuid: string, entity: Entity, metadata: Metadata) { + /** + * Begins pulling the rest of a chunked entity. + * + * `firstChunkEnd` is how many elements really arrived: a producer still filling its buffer + * sends fewer than `chunk_size`, so the nominal size would leave a hole. + */ + start(uuid: string, entity: Entity, metadata: Metadata, firstChunkEnd?: number) { const chunks = metadata.chunks if (!chunks || chunks.total <= 0) return - entity.add(traits.ChunkProgress({ loaded: chunks.chunkSize, total: chunks.total })) - void pull(uuid, entity, chunks.total, chunks.chunkSize) + const loaded = firstChunkEnd ?? chunks.chunkSize + + entity.add(traits.ChunkProgress({ loaded, total: chunks.total })) + void pull(uuid, entity, chunks.total, loaded) }, dispose() { diff --git a/src/lib/draw.ts b/src/lib/draw.ts index 1d23a4f80..c054259b2 100644 --- a/src/lib/draw.ts +++ b/src/lib/draw.ts @@ -1,4 +1,5 @@ -import type { TransformWithUUID } from '@viamrobotics/sdk' +import type { JsonValue } from '@bufbuild/protobuf' +import type { PlainMessage, Struct, TransformWithUUID } from '@viamrobotics/sdk' import type { ConfigurableTrait, Entity, Trait, World } from 'koota' import { Matrix4, Vector3, Vector4 } from 'three' @@ -6,7 +7,7 @@ import { NURBSCurve } from 'three/addons/curves/NURBSCurve.js' import { UuidTool } from 'uuid-tool' import type { Transform as TransformProto } from '$lib/buf/common/v1/common_pb' -import type { Drawing, Model, Shape } from '$lib/buf/draw/v1/drawing_pb' +import type { Model, Shape } from '$lib/buf/draw/v1/drawing_pb' import type { Relationship } from '$lib/metadata' import { @@ -16,6 +17,7 @@ import { updateBufferGeometryColors, writeBufferGeometryRange, } from '$lib/attribute' +import { DrawingProjection } from '$lib/buf/draw/v1/worldstate_pb' import { asFloat32Array, asOpacity, @@ -27,8 +29,9 @@ import { } from '$lib/buffer' import { hierarchy, relations, traits } from '$lib/ecs' import { parsePcdInWorker } from '$lib/loaders/pcd' -import { Pose } from '$lib/math' +import { Pose, type PosePatch } from '$lib/math' import { type Metadata, metadataFromStruct } from '$lib/metadata' +import { unwrapValue } from '$lib/struct' import { ColorFormat } from './buf/draw/v1/metadata_pb' import { isPointCloud } from './geometry' @@ -53,6 +56,46 @@ const DEFAULT_OPACITY = 1 export type Transform = TransformWithUUID | TransformProto +/** + * The parts of a `Drawing` the renderers actually read. A `Drawing` is structurally assignable + * to this, so a world state store `Transform` can render through the same code. + */ +export interface DrawingLike { + referenceFrame: string + poseInObserverFrame?: { referenceFrame?: string; pose?: PosePatch } + physicalObject?: Shape + uuid?: Uint8Array + metadata?: Metadata +} + +/** The metadata Struct key that carries a projected Shape. */ +const SHAPE_KEY = 'shape' + +/** + * Reads the `Shape` a world state store carries for a projected `Drawing`, or `undefined` when + * the transform is a real one. + * + * `Transform.physical_object` is a closed `Geometry` oneof with no case for a drawing shape, so + * the shape travels in the metadata `Struct` as a `DrawingProjection` instead. + */ +export const shapeFromStruct = (fields: PlainMessage['fields'] = {}): Shape | undefined => { + const value = fields[SHAPE_KEY] + if (!value) return undefined + + try { + // A producer built against a newer drawing.proto would otherwise fail the whole entity + // rather than the one field the reader does not know about yet. + const projection = DrawingProjection.fromJson( + { [SHAPE_KEY]: unwrapValue(value) as JsonValue }, + { ignoreUnknownFields: true } + ) + return projection.shape + } catch (error) { + console.error('Could not decode a projected drawing shape:', error) + return undefined + } +} + export const uuidBytesToString = (bytes: Uint8Array | undefined): string | undefined => { if (!bytes || bytes.length === 0) return undefined return UuidTool.toString([...bytes]) @@ -68,13 +111,13 @@ interface DrawOptions { removable?: boolean } -type ModelDrawing = Drawing & { +type ModelDrawing = DrawingLike & { physicalObject: Shape & { geometryType: { case: 'model'; value: Model } } } -const isModel = (drawing: Drawing): drawing is ModelDrawing => { +const isModel = (drawing: DrawingLike): drawing is ModelDrawing => { return drawing.physicalObject?.geometryType?.case === 'model' } @@ -138,7 +181,7 @@ export interface DrawingResult { export const drawDrawing = ( world: World, - drawing: Drawing, + drawing: DrawingLike, api: Trait, { removable = true }: DrawOptions = {} ): DrawingResult => { @@ -234,7 +277,7 @@ export const updateMetadata = ( export const updateDrawing = ( world: World, entity: Entity, - drawing: Drawing, + drawing: DrawingLike, { removable = true }: DrawOptions = {} ): DrawingResult => { const { poseInObserverFrame, metadata } = drawing @@ -268,7 +311,7 @@ export const updateDrawing = ( export const updateModel = ( world: World, entity: Entity, - drawing: Drawing, + drawing: DrawingLike, api: Trait, { removable = true }: DrawOptions = {} ): DrawingResult => { @@ -277,7 +320,7 @@ export const updateModel = ( return drawDrawing(world, drawing, api, { removable }) } -const applyShape = (entity: Entity, { physicalObject, metadata }: Drawing): void => { +const applyShape = (entity: Entity, { physicalObject, metadata }: DrawingLike): void => { const colors = metadata?.colors const opacities = metadata?.opacities const geometryType = physicalObject?.geometryType @@ -528,7 +571,7 @@ const parseColors = (from: Uint8Array | undefined, count: number): Uint8Array => return expanded } -const updateShape = (entity: Entity, { physicalObject, metadata }: Drawing): void => { +const updateShape = (entity: Entity, { physicalObject, metadata }: DrawingLike): void => { const geometryType = physicalObject?.geometryType entity.set(traits.Opacity, asOpacity(metadata?.opacities, DEFAULT_OPACITY)) diff --git a/src/lib/hooks/useWorldState.svelte.ts b/src/lib/hooks/useWorldState.svelte.ts index 78f1e2b34..fb7b397ad 100644 --- a/src/lib/hooks/useWorldState.svelte.ts +++ b/src/lib/hooks/useWorldState.svelte.ts @@ -14,9 +14,20 @@ import { useResourceNames, } from '@viamrobotics/svelte-sdk' -import { asFloat32Array, inMeters } from '$lib/buffer' +import type { Shape } from '$lib/buf/draw/v1/drawing_pb' +import type { Metadata } from '$lib/metadata' + +import { asFloat32Array, inMeters, STRIDE } from '$lib/buffer' import { createChunkLoader, type EntityChunk } from '$lib/chunking' -import { drawTransform, updateMetadata } from '$lib/draw' +import { + drawDrawing, + type DrawingLike, + drawTransform, + shapeFromStruct, + updateDrawing, + updateMetadata, + updateModel, +} from '$lib/draw' import { hierarchy, traits, useWorld } from '$lib/ecs' import { isPointCloud } from '$lib/geometry' import { Pose } from '$lib/math' @@ -63,6 +74,9 @@ export const provideWorldStates = () => { const snakeToCamel = (path: string): string => path.replaceAll(/_([a-z])/g, (_, char: string) => char.toUpperCase()) +/** Stands in for an empty FieldMask, which means every field changed. */ +const ALL_TRANSFORM_PATHS = ['pose_in_observer_frame', 'physical_object', 'metadata'] + const decodeBase64 = (encoded: string): Uint8Array => { const binary = atob(encoded) const bytes = new Uint8Array(binary.length) @@ -132,6 +146,9 @@ const createWorldState = (client: { current: WorldStateStoreClient | undefined } const relationships = useRelationships() const entities = new Map() + // UUIDs of projected model drawings, which spawn a root plus one sub-entity per asset and + // so cannot be torn down with a plain destroy. + const modelRoots = new Set() // UUIDs the stream has removed; guards against a stale initial snapshot or a // self-heal fetch re-creating an entity the server has already deleted. const removedUUIDs = new Set() @@ -159,20 +176,57 @@ const createWorldState = (client: { current: WorldStateStoreClient | undefined } }, }) + /** + * Builds the drawing a projected transform stands for. Frame, pose and UUID ride natively; + * only the shape had to travel in the metadata Struct. + */ + const toDrawing = ( + transform: TransformWithUUID, + shape: Shape, + metadata: Metadata + ): DrawingLike => ({ + referenceFrame: transform.referenceFrame, + poseInObserverFrame: transform.poseInObserverFrame, + physicalObject: shape, + uuid: transform.uuid, + metadata, + }) + + /** + * Element count of the first chunk that actually arrived, which is fewer than `chunk_size` + * when the producer was still filling its buffer. + */ + const firstChunkEnd = (shape: Shape | undefined): number | undefined => { + if (shape?.geometryType.case !== 'points') return undefined + return ( + shape.geometryType.value.positions.length / + (STRIDE.POSITIONS * Float32Array.BYTES_PER_ELEMENT) + ) + } + const spawnEntity = (transform: TransformWithUUID) => { - if (entities.has(transform.uuidString) || removedUUIDs.has(transform.uuidString)) { + const uuid = transform.uuidString + if (entities.has(uuid) || removedUUIDs.has(uuid)) { return } - const spawned = drawTransform(world, transform, traits.WorldStateStoreAPI, { removable: false }) - entities.set(transform.uuidString, spawned.entity) + const parsedMetadata = metadataFromStruct(transform.metadata?.fields) + const shape = shapeFromStruct(transform.metadata?.fields) + + const spawned = shape + ? drawDrawing(world, toDrawing(transform, shape, parsedMetadata), traits.WorldStateStoreAPI, { + removable: false, + }) + : drawTransform(world, transform, traits.WorldStateStoreAPI, { removable: false }) + + entities.set(uuid, spawned.entity) + if (shape?.geometryType.case === 'model') modelRoots.add(uuid) relationships.apply(spawned.entity, spawned.relationships) - const parsedMetadata = metadataFromStruct(transform.metadata?.fields) - chunkLoader.start(transform.uuidString, spawned.entity, parsedMetadata) - relationships.flush(transform.uuidString) + chunkLoader.start(uuid, spawned.entity, parsedMetadata, firstChunkEnd(shape)) + relationships.flush(uuid) - if (isPointCloud(transform.physicalObject?.geometryType)) invalidate() + if (shape || isPointCloud(transform.physicalObject?.geometryType)) invalidate() } const destroyEntity = (uuid: string) => { @@ -183,8 +237,17 @@ const createWorldState = (client: { current: WorldStateStoreClient | undefined } if (!entity) return if (world.has(entity)) { - entity.destroy() + // A model is a root plus one sub-entity per asset; destroying only the root would leave + // its meshes at the world origin. Everything else keeps the plain destroy, so frames + // parented to it survive as orphans. + if (modelRoots.has(uuid)) { + hierarchy.destroyEntityTree(world, entity) + } else { + entity.destroy() + } } + + modelRoots.delete(uuid) entities.delete(uuid) } @@ -208,6 +271,41 @@ const createWorldState = (client: { current: WorldStateStoreClient | undefined } } } + /** + * Re-applies a projected drawing wholesale. + * + * Its shape, colors and visibility all live in one metadata Struct, so any change to them + * arrives as a single `metadata` path. There is nothing finer to act on. + */ + const updateProjectedDrawing = ( + transform: TransformWithUUID, + entity: Entity, + shape: Shape, + metadata: Metadata + ) => { + const uuid = transform.uuidString + + // A chunked drawing restarts its pull from scratch, so the half-filled buffer the previous + // pull was writing into has to go with it. + if ((metadata.chunks?.total ?? 0) > 0) { + destroyEntity(uuid) + removedUUIDs.delete(uuid) + spawnEntity(transform) + return + } + + const drawing = toDrawing(transform, shape, metadata) + const isModel = shape.geometryType.case === 'model' + + const result = isModel + ? updateModel(world, entity, drawing, traits.WorldStateStoreAPI, { removable: false }) + : updateDrawing(world, entity, drawing, { removable: false }) + + entities.set(uuid, result.entity) + if (isModel) modelRoots.add(uuid) + relationships.apply(result.entity, result.relationships) + } + const updateEntity = (transform: TransformWithUUID, changes: (string | number)[]) => { const entity = entities.get(transform.uuidString) @@ -216,9 +314,24 @@ const createWorldState = (client: { current: WorldStateStoreClient | undefined } return } + const shape = shapeFromStruct(transform.metadata?.fields) + if (shape) { + updateProjectedDrawing( + transform, + entity, + shape, + metadataFromStruct(transform.metadata?.fields) + ) + return + } + let metadataDirty = false - for (const rawPath of changes) { + // An empty mask means every field changed, per the FieldMask convention — re-adding an + // existing UUID reports exactly that. Acting path by path would drop the whole redraw. + const paths = changes.length > 0 ? changes : ALL_TRANSFORM_PATHS + + for (const rawPath of paths) { if (typeof rawPath !== 'string') continue const path = snakeToCamel(rawPath) @@ -354,10 +467,16 @@ const createWorldState = (client: { current: WorldStateStoreClient | undefined } if (rafId) cancelAnimationFrame(rafId) pendingEvents = [] chunkLoader.dispose() - for (const [, entity] of entities) { - if (world.has(entity)) { + for (const [uuid, entity] of entities) { + if (!world.has(entity)) continue + + if (modelRoots.has(uuid)) { + hierarchy.destroyEntityTree(world, entity) + } else { entity.destroy() } } + entities.clear() + modelRoots.clear() } } diff --git a/src/lib/metadata.ts b/src/lib/metadata.ts index 85be90254..c3b2cd841 100644 --- a/src/lib/metadata.ts +++ b/src/lib/metadata.ts @@ -5,6 +5,7 @@ import { Metadata as MetadataProto, type Relationship as RelationshipProto, } from '$lib/buf/draw/v1/metadata_pb' +import { unwrapValue } from '$lib/struct' /** Metadata for a `Drawing` or `Transform`. Relationships default to empty. */ export type Metadata = Omit, 'relationships'> & { @@ -133,38 +134,3 @@ export const metadataFromStruct = (fields: PlainMessage['fields'] = {}): return json } - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const unwrapValue = (value: PlainMessage): unknown => { - if (!value?.kind) return value - - switch (value.kind.case) { - case 'numberValue': - case 'stringValue': - case 'boolValue': { - return value.kind.value - } - case 'structValue': { - const result: Record = {} - for (const [key, val] of Object.entries(value.kind.value.fields || {})) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - result[key] = unwrapValue(val as PlainMessage) - } - return result - } - case 'listValue': { - return ( - value.kind.value.values?.map( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (v: PlainMessage) => unwrapValue(v) - ) || [] - ) - } - case 'nullValue': { - return null - } - default: { - return value.kind.value - } - } -} diff --git a/src/lib/struct.ts b/src/lib/struct.ts new file mode 100644 index 000000000..f84ea731f --- /dev/null +++ b/src/lib/struct.ts @@ -0,0 +1,34 @@ +import type { Value } from '@bufbuild/protobuf' +import type { PlainMessage } from '@viamrobotics/sdk' + +/** + * Converts a `google.protobuf.Value` into plain JavaScript. A `Struct`'s fields are wrapped + * `Value`s, so anything handing a struct payload to a generated `fromJson` unwraps it first. + */ +export const unwrapValue = (value: PlainMessage): unknown => { + if (!value?.kind) return value + + switch (value.kind.case) { + case 'numberValue': + case 'stringValue': + case 'boolValue': { + return value.kind.value + } + case 'structValue': { + const result: Record = {} + for (const [key, val] of Object.entries(value.kind.value.fields || {})) { + result[key] = unwrapValue(val as PlainMessage) + } + return result + } + case 'listValue': { + return value.kind.value.values?.map((v) => unwrapValue(v as PlainMessage)) ?? [] + } + case 'nullValue': { + return null + } + default: { + return value.kind.value + } + } +} diff --git a/worldstate/command.go b/worldstate/command.go new file mode 100644 index 000000000..0e6d5854e --- /dev/null +++ b/worldstate/command.go @@ -0,0 +1,157 @@ +// Package worldstate carries the draw API over Viam's world_state_store service. +// +// It holds the two halves of that transport: the DoCommand payloads a writer sends, and the +// projection of a draw.v1.Drawing onto the viam.common.v1.Transform the API speaks. +package worldstate + +import ( + "encoding/json" + "errors" + "fmt" + + drawv1 "github.com/viam-labs/motion-tools/draw/v1" + commonv1 "go.viam.com/api/common/v1" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/fieldmaskpb" +) + +// ErrNilEntity is returned when a command is built from a nil drawing or transform. +var ErrNilEntity = errors.New("cannot build a command from a nil entity") + +// Keys of a DoCommand payload: the method name and its protojson request. +const ( + CommandKey = "command" + RequestKey = "request" +) + +// Every command names a draw.v1.DrawService method, and its request travels as protojson of +// that method's request message. +const ( + CommandAddEntity = "AddEntity" + CommandAddEntities = "AddEntities" + CommandUpdateEntity = "UpdateEntity" + CommandRemoveEntity = "RemoveEntity" + CommandRemoveAll = "RemoveAll" + CommandRemoveAllDrawings = "RemoveAllDrawings" + CommandRemoveAllTransforms = "RemoveAllTransforms" + CommandCreateRelationship = "CreateRelationship" + CommandDeleteRelationship = "DeleteRelationship" + CommandSetScene = "SetScene" + CommandGetEntityChunk = "GetEntityChunk" +) + +// CommandGetEntityChunkFlat is the one request that is not tunnelled. The visualizer already +// sends chunked reads in this flat shape, so a store accepts it verbatim. +const CommandGetEntityChunkFlat = "get_entity_chunk" + +// Proto field names make a logged command read like the .proto file, and numeric enums keep the +// metadata Struct contract identical on both halves of the wire. +var commandMarshal = protojson.MarshalOptions{UseProtoNames: true, UseEnumNumbers: true} + +// Command builds the DoCommand payload for a draw.v1.DrawService method. The request travels as +// protojson, so draw.v1 stays the only definition of the vocabulary. +// +// drawing := line.Draw("my-line", draw.WithID("my-line")) +// cmd, err := worldstate.AddDrawingCommand(drawing.ToProto()) +// _, err = store.DoCommand(ctx, cmd) +func Command(method string, request proto.Message) (map[string]any, error) { + command := map[string]any{CommandKey: method} + + if request == nil { + return command, nil + } + + encoded, err := commandMarshal.Marshal(request) + if err != nil { + return nil, fmt.Errorf("encoding %s request: %w", method, err) + } + + fields := map[string]any{} + if err := json.Unmarshal(encoded, &fields); err != nil { + return nil, fmt.Errorf("re-encoding %s request: %w", method, err) + } + + command[RequestKey] = fields + + return command, nil +} + +// AddDrawingCommand adds a Drawing built by the draw package's shape builders. +func AddDrawingCommand(drawing *drawv1.Drawing) (map[string]any, error) { + if drawing == nil { + return nil, ErrNilEntity + } + + return Command(CommandAddEntity, &drawv1.AddEntityRequest{ + Entity: &drawv1.AddEntityRequest_Drawing{Drawing: drawing}, + }) +} + +// AddTransformCommand adds a Transform, such as one from NewTransform or DrawnGeometry.Draw. +func AddTransformCommand(transform *commonv1.Transform) (map[string]any, error) { + if transform == nil { + return nil, ErrNilEntity + } + + return Command(CommandAddEntity, &drawv1.AddEntityRequest{ + Entity: &drawv1.AddEntityRequest_Transform{Transform: transform}, + }) +} + +// UpdateDrawingCommand patches the listed draw.DrawingPath* paths on an existing Drawing. An +// empty list replaces the entity wholesale, or on a chunked entity appends the next chunk. +func UpdateDrawingCommand(uuid []byte, drawing *drawv1.Drawing, paths ...string) (map[string]any, error) { + if drawing == nil { + return nil, ErrNilEntity + } + + return Command(CommandUpdateEntity, &drawv1.UpdateEntityRequest{ + Uuid: uuid, + Entity: &drawv1.UpdateEntityRequest_Drawing{Drawing: drawing}, + UpdatedFields: fieldMask(paths), + }) +} + +// UpdateTransformCommand patches the listed draw.TransformPath* paths on an existing Transform. +// An empty list replaces the entity wholesale. +func UpdateTransformCommand(uuid []byte, transform *commonv1.Transform, paths ...string) (map[string]any, error) { + if transform == nil { + return nil, ErrNilEntity + } + + return Command(CommandUpdateEntity, &drawv1.UpdateEntityRequest{ + Uuid: uuid, + Entity: &drawv1.UpdateEntityRequest_Transform{Transform: transform}, + UpdatedFields: fieldMask(paths), + }) +} + +// RemoveEntityCommand removes one entity by UUID. +func RemoveEntityCommand(uuid []byte) (map[string]any, error) { + return Command(CommandRemoveEntity, &drawv1.RemoveEntityRequest{Uuid: uuid}) +} + +// RemoveAllCommand clears the scene. world_state_store has no bulk-removal signal, so a store +// fans this out into one removal event per entity. +func RemoveAllCommand() map[string]any { + return map[string]any{CommandKey: CommandRemoveAll} +} + +// GetEntityChunkCommand fetches a chunked entity's data from an element offset, in the flat +// shape the visualizer sends: uuid is the canonical 36-character string, not base64. +func GetEntityChunkCommand(uuid string, start uint32) map[string]any { + return map[string]any{ + CommandKey: CommandGetEntityChunkFlat, + "uuid": uuid, + "start": float64(start), + } +} + +func fieldMask(paths []string) *fieldmaskpb.FieldMask { + if len(paths) == 0 { + return nil + } + + return &fieldmaskpb.FieldMask{Paths: paths} +} diff --git a/worldstate/command_test.go b/worldstate/command_test.go new file mode 100644 index 000000000..6e8393923 --- /dev/null +++ b/worldstate/command_test.go @@ -0,0 +1,124 @@ +package worldstate + +import ( + "encoding/json" + "testing" + + "github.com/golang/geo/r3" + "github.com/viam-labs/motion-tools/draw" + drawv1 "github.com/viam-labs/motion-tools/draw/v1" + commonv1 "go.viam.com/api/common/v1" + "go.viam.com/rdk/spatialmath" + "go.viam.com/test" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" +) + +// decodeRequest reverses what a module does with a command payload, so these tests assert the +// round trip a caller actually depends on rather than the JSON's exact shape. +func decodeRequest(t *testing.T, cmd map[string]any, into proto.Message) { + t.Helper() + + request, ok := cmd[RequestKey].(map[string]any) + test.That(t, ok, test.ShouldBeTrue) + + encoded, err := json.Marshal(request) + test.That(t, err, test.ShouldBeNil) + + test.That(t, protojson.Unmarshal(encoded, into), test.ShouldBeNil) +} + +func TestAddDrawingCommandRoundTripsABuiltLine(t *testing.T) { + line, err := draw.NewLine( + []r3.Vector{{X: -250}, {Z: 200}, {X: 250}}, + draw.WithLineWidth(12), + draw.WithSingleLineColor(draw.NewColor(draw.WithRGB(0, 120, 255))), + ) + test.That(t, err, test.ShouldBeNil) + + built := line.Draw("my-line", draw.WithID("my-line")).ToProto() + + cmd, err := AddDrawingCommand(built) + test.That(t, err, test.ShouldBeNil) + test.That(t, cmd[CommandKey], test.ShouldEqual, CommandAddEntity) + + decoded := &drawv1.AddEntityRequest{} + decodeRequest(t, cmd, decoded) + + test.That(t, proto.Equal(decoded.GetDrawing(), built), test.ShouldBeTrue) + test.That(t, decoded.GetDrawing().GetPhysicalObject().GetLine().GetLineWidth(), test.ShouldEqual, float32(12)) +} + +func TestAddTransformCommandRoundTripsABuiltGeometry(t *testing.T) { + box, err := spatialmath.NewBox(spatialmath.NewZeroPose(), r3.Vector{X: 300, Y: 300, Z: 300}, "my-box") + test.That(t, err, test.ShouldBeNil) + + drawn, err := draw.NewDrawnGeometry(box, draw.WithGeometryColor(draw.NewColor(draw.WithRGB(255, 90, 90)))) + test.That(t, err, test.ShouldBeNil) + + built, err := drawn.Draw("my-box", draw.WithID("my-box")) + test.That(t, err, test.ShouldBeNil) + + cmd, err := AddTransformCommand(built) + test.That(t, err, test.ShouldBeNil) + + decoded := &drawv1.AddEntityRequest{} + decodeRequest(t, cmd, decoded) + + test.That(t, proto.Equal(decoded.GetTransform(), built), test.ShouldBeTrue) +} + +func TestUpdateCommandsCarryTheirMask(t *testing.T) { + uuid := draw.NewDrawConfig("my-line", draw.WithID("my-line")).UUID + + cmd, err := UpdateDrawingCommand(uuid, &drawv1.Drawing{ReferenceFrame: "my-line"}, draw.DrawingPathMetadataColors) + test.That(t, err, test.ShouldBeNil) + + decoded := &drawv1.UpdateEntityRequest{} + decodeRequest(t, cmd, decoded) + + test.That(t, decoded.GetUuid(), test.ShouldResemble, uuid) + test.That(t, decoded.GetUpdatedFields().GetPaths(), test.ShouldResemble, []string{draw.DrawingPathMetadataColors}) + + transformCmd, err := UpdateTransformCommand(uuid, &commonv1.Transform{ReferenceFrame: "my-box"}, draw.TransformPathPoseValue) + test.That(t, err, test.ShouldBeNil) + + decodedTransform := &drawv1.UpdateEntityRequest{} + decodeRequest(t, transformCmd, decodedTransform) + + test.That(t, decodedTransform.GetUpdatedFields().GetPaths(), test.ShouldResemble, []string{draw.TransformPathPoseValue}) +} + +// An empty mask is a wholesale replace, and on a chunked entity it is a chunk append. Either +// way the field must be absent rather than an empty list. +func TestUpdateCommandOmitsAnEmptyMask(t *testing.T) { + cmd, err := UpdateDrawingCommand(nil, &drawv1.Drawing{ReferenceFrame: "cloud"}) + test.That(t, err, test.ShouldBeNil) + + request, ok := cmd[RequestKey].(map[string]any) + test.That(t, ok, test.ShouldBeTrue) + + _, present := request["updated_fields"] + test.That(t, present, test.ShouldBeFalse) +} + +func TestCommandsWithoutARequest(t *testing.T) { + test.That(t, RemoveAllCommand(), test.ShouldResemble, map[string]any{CommandKey: CommandRemoveAll}) + + // The flat form, not the tunnelled CommandGetEntityChunk: uuid stays a plain string. + chunk := GetEntityChunkCommand("a2e1a1e6-0000-4000-8000-000000000000", 200) + test.That(t, chunk[CommandKey], test.ShouldEqual, CommandGetEntityChunkFlat) + test.That(t, chunk["uuid"], test.ShouldEqual, "a2e1a1e6-0000-4000-8000-000000000000") + test.That(t, chunk["start"], test.ShouldEqual, float64(200)) +} + +func TestCommandsRejectNilEntities(t *testing.T) { + _, err := AddDrawingCommand(nil) + test.That(t, err, test.ShouldBeError, ErrNilEntity) + + _, err = AddTransformCommand(nil) + test.That(t, err, test.ShouldBeError, ErrNilEntity) + + _, err = UpdateDrawingCommand(nil, nil) + test.That(t, err, test.ShouldBeError, ErrNilEntity) +} diff --git a/worldstate/projection.go b/worldstate/projection.go new file mode 100644 index 000000000..33a23baa9 --- /dev/null +++ b/worldstate/projection.go @@ -0,0 +1,147 @@ +package worldstate + +import ( + "encoding/json" + "errors" + "fmt" + "maps" + "strings" + + "github.com/viam-labs/motion-tools/draw" + drawv1 "github.com/viam-labs/motion-tools/draw/v1" + commonv1 "go.viam.com/api/common/v1" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" +) + +// ErrNilDrawing is returned when a nil Drawing is projected onto a Transform. +var ErrNilDrawing = errors.New("cannot project a nil drawing") + +// projectionShapeKey is the metadata Struct key that carries a projected Shape. It is the JSON +// name of draw.v1.DrawingProjection's only field. +const projectionShapeKey = "shape" + +// The metadata Struct contract predates protojson on this path: proto field names, numeric +// enums. These options reproduce it, so neither side needs a hand-written encoder. +var ( + projectionMarshal = protojson.MarshalOptions{UseProtoNames: true, UseEnumNumbers: true} + projectionUnmarshal = protojson.UnmarshalOptions{DiscardUnknown: true} +) + +// ProjectDrawing renders a Drawing as a viam.common.v1.Transform so it can travel over the +// world_state_store API. +// +// Frame, pose and UUID ride natively, keeping field-mask deltas and the frame system working; +// the Shape and Metadata share the metadata Struct. +func ProjectDrawing(drawing *drawv1.Drawing) (*commonv1.Transform, error) { + if drawing == nil { + return nil, ErrNilDrawing + } + + fields := map[string]any{} + + if metadata := drawing.GetMetadata(); metadata != nil { + if err := mergeProtoJSON(fields, metadata); err != nil { + return nil, fmt.Errorf("encoding drawing metadata: %w", err) + } + } + + if shape := drawing.GetPhysicalObject(); shape != nil { + if err := mergeProtoJSON(fields, &drawv1.DrawingProjection{Shape: shape}); err != nil { + return nil, fmt.Errorf("encoding drawing shape: %w", err) + } + } + + metadata, err := structpb.NewStruct(fields) + if err != nil { + return nil, fmt.Errorf("building transform metadata: %w", err) + } + + // physical_object stays empty: no Drawing shape maps onto a common.v1.Geometry case. + return &commonv1.Transform{ + ReferenceFrame: drawing.GetReferenceFrame(), + PoseInObserverFrame: drawing.GetPoseInObserverFrame(), + Uuid: drawing.GetUuid(), + Metadata: metadata, + }, nil +} + +// ProjectedShape returns the Shape carried in a projected Transform's metadata, or nil when the +// Transform is not a projected Drawing. +func ProjectedShape(metadata *structpb.Struct) (*drawv1.Shape, error) { + value := metadata.GetFields()[projectionShapeKey] + if value == nil { + return nil, nil + } + + encoded, err := projectionMarshal.Marshal(value) + if err != nil { + return nil, fmt.Errorf("re-encoding projected shape: %w", err) + } + + shape := &drawv1.Shape{} + if err := projectionUnmarshal.Unmarshal(encoded, shape); err != nil { + return nil, fmt.Errorf("decoding projected shape: %w", err) + } + + return shape, nil +} + +// ProjectFieldMask rewrites a Drawing's updated-field paths for its projected Transform, +// preserving order and dropping duplicates. Transform events must not be run through this. +func ProjectFieldMask(paths []string) []string { + // An empty mask means every field changed, which is what re-adding an existing UUID + // reports. A consumer acting path by path would otherwise treat it as a no-op. + if len(paths) == 0 { + return []string{draw.TransformPathPose, draw.TransformPathMetadata} + } + + projected := make([]string, 0, len(paths)) + seen := make(map[string]struct{}, len(paths)) + + for _, path := range paths { + var mapped string + + switch { + case matchesPath(path, draw.DrawingPathPose): + mapped = path + // Shape and metadata share one Struct, so everything but the pose collapses into it. + case matchesPath(path, draw.DrawingPathShape), matchesPath(path, draw.DrawingPathMetadata): + mapped = draw.TransformPathMetadata + default: + continue + } + + if _, ok := seen[mapped]; ok { + continue + } + seen[mapped] = struct{}{} + projected = append(projected, mapped) + } + + return projected +} + +// matchesPath reports whether path is root or a field nested under it. +func matchesPath(path, root string) bool { + return path == root || strings.HasPrefix(path, root+".") +} + +// mergeProtoJSON encodes msg with the projection options and copies its top-level JSON fields +// into dst, so several messages can share one metadata Struct. +func mergeProtoJSON(dst map[string]any, msg proto.Message) error { + encoded, err := projectionMarshal.Marshal(msg) + if err != nil { + return err + } + + decoded := map[string]any{} + if err := json.Unmarshal(encoded, &decoded); err != nil { + return err + } + + maps.Copy(dst, decoded) + + return nil +} diff --git a/worldstate/projection_test.go b/worldstate/projection_test.go new file mode 100644 index 000000000..93a9dbfda --- /dev/null +++ b/worldstate/projection_test.go @@ -0,0 +1,193 @@ +package worldstate + +import ( + "encoding/base64" + "testing" + + "github.com/viam-labs/motion-tools/draw" + drawv1 "github.com/viam-labs/motion-tools/draw/v1" + commonv1 "go.viam.com/api/common/v1" + "go.viam.com/test" + "google.golang.org/protobuf/proto" +) + +func lineDrawing() *drawv1.Drawing { + return &drawv1.Drawing{ + ReferenceFrame: "my-line", + PoseInObserverFrame: &commonv1.PoseInFrame{ + ReferenceFrame: "world", + Pose: &commonv1.Pose{X: 1, Y: 2, Z: 3, OZ: 1}, + }, + Uuid: []byte("0123456789abcdef"), + PhysicalObject: &drawv1.Shape{ + Label: "a line", + GeometryType: &drawv1.Shape_Line{ + Line: &drawv1.Line{ + Positions: []byte{1, 2, 3, 4}, + LineWidth: proto.Float32(7), + }, + }, + }, + Metadata: &drawv1.Metadata{ + Colors: []byte{10, 20, 30}, + ColorFormat: drawv1.ColorFormat_COLOR_FORMAT_RGB, + Opacities: []byte{255}, + Invisible: proto.Bool(true), + }, + } +} + +func TestProjectDrawingCarriesIdentityNatively(t *testing.T) { + drawing := lineDrawing() + + transform, err := ProjectDrawing(drawing) + test.That(t, err, test.ShouldBeNil) + + test.That(t, transform.GetReferenceFrame(), test.ShouldEqual, "my-line") + test.That(t, transform.GetUuid(), test.ShouldResemble, drawing.GetUuid()) + test.That(t, transform.GetPoseInObserverFrame().GetReferenceFrame(), test.ShouldEqual, "world") + test.That(t, transform.GetPoseInObserverFrame().GetPose().GetX(), test.ShouldEqual, 1.0) + + // No Drawing shape maps onto a common.v1.Geometry case, so the slot stays empty. + test.That(t, transform.GetPhysicalObject(), test.ShouldBeNil) +} + +// The metadata Struct contract is consumed by the frontend's metadataFromStruct, which reads +// proto field names, base64 bytes and numeric enums. This pins that encoding. +func TestProjectDrawingMetadataMatchesStructContract(t *testing.T) { + transform, err := ProjectDrawing(lineDrawing()) + test.That(t, err, test.ShouldBeNil) + + fields := transform.GetMetadata().GetFields() + + test.That(t, fields["colors"].GetStringValue(), test.ShouldEqual, + base64.StdEncoding.EncodeToString([]byte{10, 20, 30})) + test.That(t, fields["opacities"].GetStringValue(), test.ShouldEqual, + base64.StdEncoding.EncodeToString([]byte{255})) + test.That(t, fields["color_format"].GetNumberValue(), test.ShouldEqual, + float64(drawv1.ColorFormat_COLOR_FORMAT_RGB)) + test.That(t, fields["invisible"].GetBoolValue(), test.ShouldBeTrue) +} + +func TestProjectDrawingChunksAndRelationshipsUseProtoNames(t *testing.T) { + drawing := lineDrawing() + drawing.Metadata.Chunks = &drawv1.Chunks{ChunkSize: 50, Total: 10_000, Stride: 12} + drawing.Metadata.Relationships = []*drawv1.Relationship{{ + TargetUuid: []byte("fedcba9876543210"), + Type: "HoverLink", + IndexMapping: proto.String("index"), + }} + + transform, err := ProjectDrawing(drawing) + test.That(t, err, test.ShouldBeNil) + + chunks := transform.GetMetadata().GetFields()["chunks"].GetStructValue().GetFields() + test.That(t, chunks["chunk_size"].GetNumberValue(), test.ShouldEqual, 50.0) + test.That(t, chunks["total"].GetNumberValue(), test.ShouldEqual, 10_000.0) + test.That(t, chunks["stride"].GetNumberValue(), test.ShouldEqual, 12.0) + + relationships := transform.GetMetadata().GetFields()["relationships"].GetListValue().GetValues() + test.That(t, relationships, test.ShouldHaveLength, 1) + + relationship := relationships[0].GetStructValue().GetFields() + test.That(t, relationship["target_uuid"].GetStringValue(), test.ShouldEqual, + base64.StdEncoding.EncodeToString([]byte("fedcba9876543210"))) + test.That(t, relationship["type"].GetStringValue(), test.ShouldEqual, "HoverLink") + test.That(t, relationship["index_mapping"].GetStringValue(), test.ShouldEqual, "index") +} + +func TestProjectedShapeRoundTripsEveryCase(t *testing.T) { + shapes := map[string]*drawv1.Shape{ + "arrows": {GeometryType: &drawv1.Shape_Arrows{ + Arrows: &drawv1.Arrows{Poses: []byte{9, 8, 7}}, + }}, + "line": {GeometryType: &drawv1.Shape_Line{ + Line: &drawv1.Line{Positions: []byte{1, 2}, LineWidth: proto.Float32(3), DotColors: []byte{4}}, + }}, + "points": {GeometryType: &drawv1.Shape_Points{ + Points: &drawv1.Points{Positions: []byte{5, 6}, PointSize: proto.Float32(11)}, + }}, + "nurbs": {GeometryType: &drawv1.Shape_Nurbs{ + Nurbs: &drawv1.Nurbs{ControlPoints: []byte{1}, Knots: []byte{2}, Degree: proto.Int32(3)}, + }}, + "model": {GeometryType: &drawv1.Shape_Model{ + Model: &drawv1.Model{ + Assets: []*drawv1.ModelAsset{{ + MimeType: "model/gltf-binary", + SizeBytes: proto.Uint64(1234), + Content: &drawv1.ModelAsset_Url{Url: "https://example.com/a.glb"}, + }}, + Scale: &commonv1.Vector3{X: 1, Y: 2, Z: 3}, + }, + }}, + } + + for name, shape := range shapes { + t.Run(name, func(t *testing.T) { + shape.Center = &commonv1.Pose{X: 5, OZ: 1} + shape.Label = name + + transform, err := ProjectDrawing(&drawv1.Drawing{PhysicalObject: shape}) + test.That(t, err, test.ShouldBeNil) + + decoded, err := ProjectedShape(transform.GetMetadata()) + test.That(t, err, test.ShouldBeNil) + test.That(t, proto.Equal(decoded, shape), test.ShouldBeTrue) + }) + } +} + +func TestProjectedShapeIgnoresTrueTransforms(t *testing.T) { + transform, err := ProjectDrawing(&drawv1.Drawing{ + Metadata: &drawv1.Metadata{Colors: []byte{1, 2, 3}}, + }) + test.That(t, err, test.ShouldBeNil) + + shape, err := ProjectedShape(transform.GetMetadata()) + test.That(t, err, test.ShouldBeNil) + test.That(t, shape, test.ShouldBeNil) + + shape, err = ProjectedShape(nil) + test.That(t, err, test.ShouldBeNil) + test.That(t, shape, test.ShouldBeNil) +} + +func TestProjectDrawingRejectsNil(t *testing.T) { + _, err := ProjectDrawing(nil) + test.That(t, err, test.ShouldBeError, ErrNilDrawing) +} + +func TestProjectFieldMask(t *testing.T) { + tests := []struct { + name string + paths []string + expected []string + }{ + {"pose passes through", []string{draw.DrawingPathPoseValue}, []string{draw.DrawingPathPoseValue}}, + {"parent passes through", []string{draw.DrawingPathPoseParent}, []string{draw.DrawingPathPoseParent}}, + {"shape becomes metadata", []string{draw.DrawingPathShape}, []string{draw.TransformPathMetadata}}, + {"metadata field collapses", []string{draw.DrawingPathMetadataColors}, []string{draw.TransformPathMetadata}}, + { + "shape and metadata dedupe", + []string{draw.DrawingPathShape, draw.DrawingPathMetadataColors, draw.DrawingPathMetadataInvisible}, + []string{draw.TransformPathMetadata}, + }, + { + "order is preserved", + []string{draw.DrawingPathMetadataColors, draw.DrawingPathPoseValue}, + []string{draw.TransformPathMetadata, draw.DrawingPathPoseValue}, + }, + {"immutable paths drop", []string{draw.DrawingPathReferenceFrame, draw.DrawingPathUUID}, []string{}}, + { + "an empty mask means everything", + nil, + []string{draw.TransformPathPose, draw.TransformPathMetadata}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + test.That(t, ProjectFieldMask(tc.paths), test.ShouldResemble, tc.expected) + }) + } +}