From e0de91ba65a8be2f0f36a27335cd27a0ed3ac58c Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 23 Jul 2026 16:58:04 +0100 Subject: [PATCH] feat(engine/ts + display/{http,socket,pipe}): CoreTS runtime engine + serve targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit go-render's TypeScript engine composing CoreTS (dappco.re/go/ts@v0.2.2) + the http/socket/pipe serve targets. All //go:build !js — the WASM binary (cmd/wasm) builds with the four new packages excluded. build+vet+real-Deno green. Co-Authored-By: Virgil --- go/display/http/doc.go | 7 + go/display/http/http.go | 93 ++++++ go/display/http/http_example_test.go | 13 + go/display/http/http_test.go | 112 ++++++++ go/display/pipe/doc.go | 6 + go/display/pipe/pipe.go | 34 +++ go/display/pipe/pipe_example_test.go | 9 + go/display/pipe/pipe_test.go | 65 +++++ go/display/socket/doc.go | 6 + go/display/socket/socket.go | 31 ++ go/display/socket/socket_example_test.go | 9 + go/display/socket/socket_test.go | 68 +++++ go/engine/ts/doc.go | 7 + go/engine/ts/engine.go | 352 +++++++++++++++++++++++ go/engine/ts/engine_example_test.go | 17 ++ go/engine/ts/engine_test.go | 256 +++++++++++++++++ go/go.mod | 29 +- go/go.sum | 90 +++++- 18 files changed, 1194 insertions(+), 10 deletions(-) create mode 100644 go/display/http/doc.go create mode 100644 go/display/http/http.go create mode 100644 go/display/http/http_example_test.go create mode 100644 go/display/http/http_test.go create mode 100644 go/display/pipe/doc.go create mode 100644 go/display/pipe/pipe.go create mode 100644 go/display/pipe/pipe_example_test.go create mode 100644 go/display/pipe/pipe_test.go create mode 100644 go/display/socket/doc.go create mode 100644 go/display/socket/socket.go create mode 100644 go/display/socket/socket_example_test.go create mode 100644 go/display/socket/socket_test.go create mode 100644 go/engine/ts/doc.go create mode 100644 go/engine/ts/engine.go create mode 100644 go/engine/ts/engine_example_test.go create mode 100644 go/engine/ts/engine_test.go diff --git a/go/display/http/doc.go b/go/display/http/doc.go new file mode 100644 index 0000000..5a175e8 --- /dev/null +++ b/go/display/http/doc.go @@ -0,0 +1,7 @@ +//go:build !js + +// SPDX-Licence-Identifier: EUPL-1.2 + +// Package httpdisplay serves server-rendered TypeScript output and static +// assets over HTTP. +package httpdisplay diff --git a/go/display/http/http.go b/go/display/http/http.go new file mode 100644 index 0000000..bef65b9 --- /dev/null +++ b/go/display/http/http.go @@ -0,0 +1,93 @@ +//go:build !js + +// SPDX-Licence-Identifier: EUPL-1.2 + +package httpdisplay + +import ( + nethttp "net/http" + + core "dappco.re/go" + tsengine "dappco.re/go/html/engine/ts" +) + +// Option configures Handler. +type Option func(*handlerOptions) + +type handlerOptions struct { + entry string +} + +// WithEntry selects the TypeScript or JavaScript SSR entry module. +func WithEntry(entry string) Option { + return func(options *handlerOptions) { + options.entry = entry + } +} + +// Handler returns an HTTP handler that serves files found under assetsDir and +// server-renders every other request through engine. +// +// Static files retain their standard content types and caching semantics. +// Rendered responses receive text/html; charset=utf-8. +func Handler(engine tsengine.Renderer, assetsDir string, opts ...Option) nethttp.Handler { + options := handlerOptions{} + for _, option := range opts { + if option != nil { + option(&options) + } + } + + var assets nethttp.Handler + if assetsDir != "" { + assets = nethttp.FileServer(nethttp.Dir(assetsDir)) + } + + return nethttp.HandlerFunc(func(writer nethttp.ResponseWriter, request *nethttp.Request) { + if assets != nil && isAssetRequest(assetsDir, request) { + assets.ServeHTTP(writer, request) + return + } + if engine == nil { + nethttp.Error(writer, "render engine unavailable", nethttp.StatusInternalServerError) + return + } + if core.Trim(options.entry) == "" { + nethttp.Error(writer, "render entry unavailable", nethttp.StatusInternalServerError) + return + } + + input := map[string]any{ + "method": request.Method, + "url": request.URL.String(), + "path": request.URL.Path, + "query": request.URL.Query(), + "headers": request.Header.Clone(), + "host": request.Host, + } + output, err := engine.Render(request.Context(), options.entry, input) + if err != nil { + nethttp.Error(writer, "render failed", nethttp.StatusInternalServerError) + return + } + + writer.Header().Set("Content-Type", "text/html; charset=utf-8") + writer.WriteHeader(nethttp.StatusOK) + _, _ = writer.Write(output) + }) +} + +func isAssetRequest(assetsDir string, request *nethttp.Request) bool { + if request.Method != nethttp.MethodGet && request.Method != nethttp.MethodHead { + return false + } + + file, err := nethttp.Dir(assetsDir).Open(request.URL.Path) + if err != nil { + return false + } + defer file.Close() + + info, err := file.Stat() + return err == nil && !info.IsDir() +} diff --git a/go/display/http/http_example_test.go b/go/display/http/http_example_test.go new file mode 100644 index 0000000..bae2c70 --- /dev/null +++ b/go/display/http/http_example_test.go @@ -0,0 +1,13 @@ +//go:build !js + +// SPDX-Licence-Identifier: EUPL-1.2 + +package httpdisplay + +func ExampleHandler() { + _ = Handler +} + +func ExampleWithEntry() { + _ = WithEntry("server.ts") +} diff --git a/go/display/http/http_test.go b/go/display/http/http_test.go new file mode 100644 index 0000000..ed91276 --- /dev/null +++ b/go/display/http/http_test.go @@ -0,0 +1,112 @@ +//go:build !js + +// SPDX-Licence-Identifier: EUPL-1.2 + +package httpdisplay + +import ( + nethttp "net/http" + "net/http/httptest" + "testing" + + core "dappco.re/go" +) + +type renderStub struct { + output []byte + err error + calls int + entry string + input any +} + +func (s *renderStub) Render(_ core.Context, entry string, input any) ([]byte, error) { + s.calls++ + s.entry = entry + s.input = input + return s.output, s.err +} + +func TestHttp_Handler_Good(t *testing.T) { + renderer := &renderStub{output: []byte("
rendered
")} + handler := Handler(renderer, "", WithEntry("server.ts")) + recorder := httptest.NewRecorder() + request := httptest.NewRequest(nethttp.MethodGet, "http://example.test/dashboard?tab=one", nil) + + handler.ServeHTTP(recorder, request) + + core.AssertEqual(t, nethttp.StatusOK, recorder.Code) + core.AssertEqual(t, "
rendered
", recorder.Body.String()) + core.AssertEqual(t, "text/html; charset=utf-8", recorder.Header().Get("Content-Type")) + core.AssertEqual(t, 1, renderer.calls) + requestInput := renderer.input.(map[string]any) + core.AssertEqual(t, "/dashboard", requestInput["path"]) +} + +func TestHttp_Handler_Bad(t *testing.T) { + renderer := &renderStub{err: core.E("test.render", "failed", nil)} + handler := Handler(renderer, "", WithEntry("broken.ts")) + recorder := httptest.NewRecorder() + request := httptest.NewRequest(nethttp.MethodGet, "http://example.test/", nil) + + handler.ServeHTTP(recorder, request) + + core.AssertEqual(t, nethttp.StatusInternalServerError, recorder.Code) + core.AssertContains(t, recorder.Body.String(), "render failed") + core.AssertEqual(t, 1, renderer.calls) +} + +func TestHttp_Handler_Ugly(t *testing.T) { + assetsDir := t.TempDir() + writeResult := core.WriteFile(core.PathJoin(assetsDir, "app.js"), []byte("asset();"), 0o600) + core.RequireTrue(t, writeResult.OK, writeResult.Error()) + renderer := &renderStub{output: []byte("
unused
")} + handler := Handler(renderer, assetsDir, WithEntry("server.ts")) + recorder := httptest.NewRecorder() + request := httptest.NewRequest(nethttp.MethodGet, "http://example.test/app.js", nil) + + handler.ServeHTTP(recorder, request) + + core.AssertEqual(t, nethttp.StatusOK, recorder.Code) + core.AssertEqual(t, "asset();", recorder.Body.String()) + core.AssertEqual(t, 0, renderer.calls) +} + +func TestHttp_WithEntry_Good(t *testing.T) { + renderer := &renderStub{output: []byte("ok")} + handler := Handler(renderer, "", WithEntry("main.server.ts")) + recorder := httptest.NewRecorder() + request := httptest.NewRequest(nethttp.MethodGet, "http://example.test/", nil) + + handler.ServeHTTP(recorder, request) + + core.AssertEqual(t, nethttp.StatusOK, recorder.Code) + core.AssertEqual(t, "main.server.ts", renderer.entry) + core.AssertEqual(t, 1, renderer.calls) +} + +func TestHttp_WithEntry_Bad(t *testing.T) { + renderer := &renderStub{output: []byte("unused")} + handler := Handler(renderer, "", WithEntry(" ")) + recorder := httptest.NewRecorder() + request := httptest.NewRequest(nethttp.MethodGet, "http://example.test/", nil) + + handler.ServeHTTP(recorder, request) + + core.AssertEqual(t, nethttp.StatusInternalServerError, recorder.Code) + core.AssertContains(t, recorder.Body.String(), "entry unavailable") + core.AssertEqual(t, 0, renderer.calls) +} + +func TestHttp_WithEntry_Ugly(t *testing.T) { + renderer := &renderStub{output: []byte("last wins")} + handler := Handler(renderer, "", WithEntry("first.ts"), WithEntry("second.ts")) + recorder := httptest.NewRecorder() + request := httptest.NewRequest(nethttp.MethodGet, "http://example.test/", nil) + + handler.ServeHTTP(recorder, request) + + core.AssertEqual(t, nethttp.StatusOK, recorder.Code) + core.AssertEqual(t, "second.ts", renderer.entry) + core.AssertEqual(t, "last wins", recorder.Body.String()) +} diff --git a/go/display/pipe/doc.go b/go/display/pipe/doc.go new file mode 100644 index 0000000..0fd4b1a --- /dev/null +++ b/go/display/pipe/doc.go @@ -0,0 +1,6 @@ +//go:build !js + +// SPDX-Licence-Identifier: EUPL-1.2 + +// Package pipe emits one server-rendered TypeScript result to a writer. +package pipe diff --git a/go/display/pipe/pipe.go b/go/display/pipe/pipe.go new file mode 100644 index 0000000..f3d88ba --- /dev/null +++ b/go/display/pipe/pipe.go @@ -0,0 +1,34 @@ +//go:build !js + +// SPDX-Licence-Identifier: EUPL-1.2 + +package pipe + +import ( + core "dappco.re/go" + tsengine "dappco.re/go/html/engine/ts" +) + +// Emit renders entry once with a nil input and writes the exact output bytes +// to writer. +func Emit(engine tsengine.Renderer, entry string, writer core.Writer) error { + if engine == nil { + return core.E("pipe.Emit", "render engine is nil", nil) + } + if writer == nil { + return core.E("pipe.Emit", "writer is nil", nil) + } + + output, err := engine.Render(core.Background(), entry, nil) + if err != nil { + return core.E("pipe.Emit", "render TypeScript entry", err) + } + written, err := writer.Write(output) + if err != nil { + return core.E("pipe.Emit", "write rendered output", err) + } + if written != len(output) { + return core.E("pipe.Emit", "short write", nil) + } + return nil +} diff --git a/go/display/pipe/pipe_example_test.go b/go/display/pipe/pipe_example_test.go new file mode 100644 index 0000000..4e7a442 --- /dev/null +++ b/go/display/pipe/pipe_example_test.go @@ -0,0 +1,9 @@ +//go:build !js + +// SPDX-Licence-Identifier: EUPL-1.2 + +package pipe + +func ExampleEmit() { + _ = Emit +} diff --git a/go/display/pipe/pipe_test.go b/go/display/pipe/pipe_test.go new file mode 100644 index 0000000..bb2ce5c --- /dev/null +++ b/go/display/pipe/pipe_test.go @@ -0,0 +1,65 @@ +//go:build !js + +// SPDX-Licence-Identifier: EUPL-1.2 + +package pipe + +import ( + "testing" + + core "dappco.re/go" +) + +type pipeRenderer struct { + output []byte + err error + entry string + input any +} + +func (r *pipeRenderer) Render(_ core.Context, entry string, input any) ([]byte, error) { + r.entry = entry + r.input = input + return r.output, r.err +} + +type shortWriter struct{} + +func (shortWriter) Write(data []byte) (int, error) { + if len(data) == 0 { + return 0, nil + } + return len(data) - 1, nil +} + +func TestPipe_Emit_Good(t *testing.T) { + renderer := &pipeRenderer{output: []byte("

ready

")} + output := core.NewBuffer() + err := Emit(renderer, "server.ts", output) + + core.AssertNoError(t, err) + core.AssertEqual(t, "

ready

", output.String()) + core.AssertEqual(t, "server.ts", renderer.entry) + core.AssertNil(t, renderer.input) +} + +func TestPipe_Emit_Bad(t *testing.T) { + renderer := &pipeRenderer{err: core.E("test.render", "failed", nil)} + output := core.NewBuffer() + err := Emit(renderer, "broken.ts", output) + + core.AssertError(t, err) + core.AssertContains(t, err.Error(), "render TypeScript entry") + core.AssertEqual(t, 0, output.Len()) + core.AssertEqual(t, "broken.ts", renderer.entry) +} + +func TestPipe_Emit_Ugly(t *testing.T) { + renderer := &pipeRenderer{output: []byte("partial")} + err := Emit(renderer, "server.ts", shortWriter{}) + + core.AssertError(t, err) + core.AssertContains(t, err.Error(), "short write") + core.AssertEqual(t, "server.ts", renderer.entry) + core.AssertNil(t, renderer.input) +} diff --git a/go/display/socket/doc.go b/go/display/socket/doc.go new file mode 100644 index 0000000..0ae8d51 --- /dev/null +++ b/go/display/socket/doc.go @@ -0,0 +1,6 @@ +//go:build !js + +// SPDX-Licence-Identifier: EUPL-1.2 + +// Package socket serves TypeScript SSR over an existing TCP or Unix listener. +package socket diff --git a/go/display/socket/socket.go b/go/display/socket/socket.go new file mode 100644 index 0000000..cf81bf7 --- /dev/null +++ b/go/display/socket/socket.go @@ -0,0 +1,31 @@ +//go:build !js + +// SPDX-Licence-Identifier: EUPL-1.2 + +package socket + +import ( + "net" + nethttp "net/http" + + core "dappco.re/go" + httpdisplay "dappco.re/go/html/display/http" + tsengine "dappco.re/go/html/engine/ts" +) + +// Serve serves the HTTP SSR handler over listener until the listener closes. +// The listener may use TCP or a Unix-domain socket. +func Serve(engine tsengine.Renderer, listener net.Listener, opts ...httpdisplay.Option) error { + if engine == nil { + return core.E("socket.Serve", "render engine is nil", nil) + } + if listener == nil { + return core.E("socket.Serve", "listener is nil", nil) + } + + err := nethttp.Serve(listener, httpdisplay.Handler(engine, "", opts...)) + if err == nil || core.Is(err, nethttp.ErrServerClosed) || core.Is(err, net.ErrClosed) { + return nil + } + return core.E("socket.Serve", "serve render listener", err) +} diff --git a/go/display/socket/socket_example_test.go b/go/display/socket/socket_example_test.go new file mode 100644 index 0000000..6c5fb5d --- /dev/null +++ b/go/display/socket/socket_example_test.go @@ -0,0 +1,9 @@ +//go:build !js + +// SPDX-Licence-Identifier: EUPL-1.2 + +package socket + +func ExampleServe() { + _ = Serve +} diff --git a/go/display/socket/socket_test.go b/go/display/socket/socket_test.go new file mode 100644 index 0000000..a892959 --- /dev/null +++ b/go/display/socket/socket_test.go @@ -0,0 +1,68 @@ +//go:build !js + +// SPDX-Licence-Identifier: EUPL-1.2 + +package socket + +import ( + nethttp "net/http" + "testing" + + core "dappco.re/go" + httpdisplay "dappco.re/go/html/display/http" +) + +type socketRenderer struct { + output []byte + entry string +} + +func (r *socketRenderer) Render(_ core.Context, entry string, _ any) ([]byte, error) { + r.entry = entry + return r.output, nil +} + +func TestSocket_Serve_Good(t *testing.T) { + listenResult := core.NetListen("tcp", "127.0.0.1:0") + core.RequireTrue(t, listenResult.OK, listenResult.Error()) + listener := listenResult.Value.(core.Listener) + renderer := &socketRenderer{output: []byte("
socket
")} + done := make(chan error, 1) + go func() { + done <- Serve(renderer, listener, httpdisplay.WithEntry("server.ts")) + }() + + client := &nethttp.Client{Timeout: 2 * core.Second} + response, err := client.Get("http://" + listener.Addr().String() + "/") + core.RequireNoError(t, err) + readResult := core.ReadAll(response.Body) + core.RequireTrue(t, readResult.OK, readResult.Error()) + core.AssertEqual(t, "
socket
", readResult.Value.(string)) + core.AssertEqual(t, "server.ts", renderer.entry) + + core.AssertNoError(t, listener.Close()) + core.AssertNoError(t, <-done) +} + +func TestSocket_Serve_Bad(t *testing.T) { + listenResult := core.NetListen("tcp", "127.0.0.1:0") + core.RequireTrue(t, listenResult.OK, listenResult.Error()) + listener := listenResult.Value.(core.Listener) + defer listener.Close() + + err := Serve(nil, listener) + + core.AssertError(t, err) + core.AssertContains(t, err.Error(), "engine is nil") + core.AssertContains(t, err.Error(), "socket.Serve") +} + +func TestSocket_Serve_Ugly(t *testing.T) { + renderer := &socketRenderer{output: []byte("unused")} + err := Serve(renderer, nil, httpdisplay.WithEntry("server.ts")) + + core.AssertError(t, err) + core.AssertContains(t, err.Error(), "listener is nil") + core.AssertEqual(t, "", renderer.entry) + core.AssertEqual(t, "unused", core.AsString(renderer.output)) +} diff --git a/go/engine/ts/doc.go b/go/engine/ts/doc.go new file mode 100644 index 0000000..e46528c --- /dev/null +++ b/go/engine/ts/doc.go @@ -0,0 +1,7 @@ +//go:build !js + +// SPDX-Licence-Identifier: EUPL-1.2 + +// Package ts renders TypeScript and JavaScript modules in a CoreTS Deno +// sidecar. It is server-only and is deliberately excluded from js/wasm builds. +package ts diff --git a/go/engine/ts/engine.go b/go/engine/ts/engine.go new file mode 100644 index 0000000..f0cbf31 --- /dev/null +++ b/go/engine/ts/engine.go @@ -0,0 +1,352 @@ +//go:build !js + +// SPDX-Licence-Identifier: EUPL-1.2 + +package ts + +import ( + core "dappco.re/go" + corets "dappco.re/go/ts" +) + +const ( + renderModulePrefix = "go-render-" + renderGroupPrefix = "go-render-ts-" +) + +// Renderer is the content-producing contract shared by the server-side +// display packages. +type Renderer interface { + Render(ctx core.Context, entry string, input any) ([]byte, error) +} + +// Engine owns a CoreTS service and its managed Deno sidecar. +type Engine struct { + mu core.RWMutex + app *core.Core + service *corets.Service + tempDir string + appRoot string + closed bool +} + +// New starts a CoreTS service and its configured Deno sidecar. +// +// SidecarArgs must launch a CoreTS-compatible Deno runtime. A typical runtime +// uses: run, -A, --unstable-worker-options, and CoreTS's runtime/main.ts. +func New(opts corets.Options) (*Engine, error) { + if len(opts.SidecarArgs) == 0 { + return nil, core.E("ts.New", "CoreTS SidecarArgs are required", nil) + } + + tempResult := core.MkdirTemp("", renderGroupPrefix) + if !tempResult.OK { + return nil, core.E("ts.New", "create render workspace", resultError(tempResult)) + } + tempDir := tempResult.Value.(string) + + if opts.SocketPath == "" { + opts.SocketPath = core.PathJoin(tempDir, "core.sock") + } + if opts.DenoSocketPath == "" { + opts.DenoSocketPath = core.PathJoin(tempDir, "deno.sock") + } + + app := core.New(core.WithService(corets.NewServiceFactory(opts))) + service, ok := core.ServiceFor[*corets.Service](app, "ts") + if !ok || service == nil { + removeResult := core.RemoveAll(tempDir) + if !removeResult.OK { + return nil, core.E("ts.New", "remove render workspace after CoreTS registration failure", resultError(removeResult)) + } + return nil, core.E("ts.New", "register CoreTS service", nil) + } + + startResult := app.ServiceStartup(core.Background(), nil) + if !startResult.OK { + shutdownResult := app.ServiceShutdown(core.Background()) + if !shutdownResult.OK { + return nil, core.E("ts.New", "clean up after CoreTS startup failure", resultError(shutdownResult)) + } + removeResult := core.RemoveAll(tempDir) + if !removeResult.OK { + return nil, core.E("ts.New", "remove render workspace after CoreTS startup failure", resultError(removeResult)) + } + return nil, core.E("ts.New", "start CoreTS service", resultError(startResult)) + } + if service.DenoClient() == nil { + shutdownResult := app.ServiceShutdown(core.Background()) + if !shutdownResult.OK { + return nil, core.E("ts.New", "stop CoreTS service without a Deno client", resultError(shutdownResult)) + } + removeResult := core.RemoveAll(tempDir) + if !removeResult.OK { + return nil, core.E("ts.New", "remove render workspace without a Deno client", resultError(removeResult)) + } + return nil, core.E("ts.New", "CoreTS Deno client is not connected", nil) + } + + return &Engine{ + app: app, + service: service, + tempDir: tempDir, + appRoot: canonicalPath(opts.AppRoot), + }, nil +} + +// Render loads a permission-scoped wrapper module in CoreTS, invokes the +// entry's render export with input, and returns its HTML or byte output. +// +// The entry may be a local .ts/.tsx/.js module or a data: module. It must +// export render(input), a default function, or a default object with a render +// method. String, []byte/Uint8Array, ArrayBuffer, Blob, Response, and +// JSON-serialisable results are supported. +func (e *Engine) Render(ctx core.Context, entry string, input any) ([]byte, error) { + if e == nil { + return nil, core.E("ts.Engine.Render", "engine is nil", nil) + } + if ctx == nil { + return nil, core.E("ts.Engine.Render", "context is nil", nil) + } + if err := ctx.Err(); err != nil { + return nil, core.E("ts.Engine.Render", "render context is done", err) + } + + e.mu.RLock() + defer e.mu.RUnlock() + + if e.closed || e.service == nil { + return nil, core.E("ts.Engine.Render", "engine is closed", nil) + } + + entryPoint, entryReads, err := e.entryPoint(entry) + if err != nil { + return nil, err + } + inputJSON, err := marshalJSON("ts.Engine.Render", "serialise render input", input) + if err != nil { + return nil, err + } + + randomResult := core.RandomString(16) + if !randomResult.OK { + return nil, core.E("ts.Engine.Render", "create module identity", resultError(randomResult)) + } + identity := renderModulePrefix + randomResult.Value.(string) + workspaceResult := core.MkdirTemp(e.tempDir, identity+"-") + if !workspaceResult.OK { + return nil, core.E("ts.Engine.Render", "create isolated render workspace", resultError(workspaceResult)) + } + renderDir := workspaceResult.Value.(string) + defer func() { + if removeResult := core.RemoveAll(renderDir); !removeResult.OK { + core.Warn("TypeScript render workspace cleanup failed", "path", renderDir, "err", resultError(removeResult)) + } + }() + wrapperPath := core.PathJoin(renderDir, "render.ts") + outputPath := core.PathJoin(renderDir, "render.out") + + wrapper, err := renderWrapper(entryPoint, inputJSON, outputPath) + if err != nil { + return nil, err + } + writeResult := core.WriteFile(wrapperPath, core.AsBytes(wrapper), 0o600) + if !writeResult.OK { + return nil, core.E("ts.Engine.Render", "write render wrapper", resultError(writeResult)) + } + + wrapperEntry := fileURL(wrapperPath) + permissions := corets.ModulePermissions{ + Read: append([]string{renderDir}, entryReads...), + Write: []string{renderDir}, + } + loadResponse, err := e.service.LoadModule(identity, wrapperEntry, permissions) + if err != nil { + return nil, core.E("ts.Engine.Render", "load TypeScript render module", err) + } + defer func() { + unloadResponse, unloadErr := e.service.UnloadModule(identity) + if unloadErr != nil { + core.Warn("TypeScript render module unload failed", "module", identity, "err", unloadErr) + return + } + if unloadResponse == nil || !unloadResponse.Ok { + core.Warn("TypeScript render module did not unload", "module", identity) + } + }() + if loadResponse == nil || !loadResponse.Ok { + message := "TypeScript render module was rejected" + if loadResponse != nil && loadResponse.Error != "" { + message = loadResponse.Error + } + return nil, core.E("ts.Engine.Render", message, nil) + } + if err := ctx.Err(); err != nil { + return nil, core.E("ts.Engine.Render", "render context is done", err) + } + + readResult := core.ReadFile(outputPath) + if !readResult.OK { + return nil, core.E("ts.Engine.Render", "read rendered output", resultError(readResult)) + } + return readResult.Value.([]byte), nil +} + +// Close stops the CoreTS service and sidecar and removes the render workspace. +// It is safe to call Close more than once. +func (e *Engine) Close() error { + if e == nil { + return core.E("ts.Engine.Close", "engine is nil", nil) + } + + e.mu.Lock() + defer e.mu.Unlock() + + if e.closed { + return nil + } + e.closed = true + + var closeErr error + if e.app != nil { + shutdownResult := e.app.ServiceShutdown(core.Background()) + if !shutdownResult.OK { + closeErr = core.E("ts.Engine.Close", "stop CoreTS service", resultError(shutdownResult)) + } + } + if e.tempDir != "" { + removeResult := core.RemoveAll(e.tempDir) + if !removeResult.OK && closeErr == nil { + closeErr = core.E("ts.Engine.Close", "remove render workspace", resultError(removeResult)) + } + } + + e.app = nil + e.service = nil + e.tempDir = "" + return closeErr +} + +func (e *Engine) entryPoint(entry string) (string, []string, error) { + entry = core.Trim(entry) + if entry == "" { + return "", nil, core.E("ts.Engine.Render", "entry is required", nil) + } + if core.HasPrefix(entry, "data:") { + return entry, nil, nil + } + + path := entry + if core.HasPrefix(entry, "file:") { + parseResult := core.URLParse(entry) + if !parseResult.OK { + return "", nil, core.E("ts.Engine.Render", "parse file entry URL", resultError(parseResult)) + } + parsed := parseResult.Value.(*core.URL) + if parsed.Scheme != "file" || (parsed.Host != "" && parsed.Host != "localhost") { + return "", nil, core.E("ts.Engine.Render", "entry must be a local file URL", nil) + } + path = parsed.Path + } else if !core.PathIsAbs(entry) { + parseResult := core.URLParse(entry) + if parseResult.OK && parseResult.Value.(*core.URL).Scheme != "" { + return "", nil, core.E("ts.Engine.Render", "remote module entries are not permitted", nil) + } + } + + absoluteResult := core.PathAbs(path) + if !absoluteResult.OK { + return "", nil, core.E("ts.Engine.Render", "resolve module entry", resultError(absoluteResult)) + } + absolute := absoluteResult.Value.(string) + resolvedResult := core.PathEvalSymlinks(absolute) + if !resolvedResult.OK { + return "", nil, core.E("ts.Engine.Render", "resolve module entry symlinks", resultError(resolvedResult)) + } + resolved := resolvedResult.Value.(string) + + statResult := core.Stat(resolved) + if !statResult.OK { + return "", nil, core.E("ts.Engine.Render", "stat module entry", resultError(statResult)) + } + if statResult.Value.(core.FsFileInfo).IsDir() { + return "", nil, core.E("ts.Engine.Render", "module entry is a directory", nil) + } + + readRoot := core.PathDir(resolved) + if e.appRoot != "" { + if !corets.CheckPath(resolved, []string{e.appRoot}) { + return "", nil, core.E("ts.Engine.Render", "module entry is outside CoreTS AppRoot", nil) + } + readRoot = e.appRoot + } + return fileURL(resolved), []string{readRoot}, nil +} + +func renderWrapper(entryPoint, inputJSON, outputPath string) (string, error) { + entryJSON, err := marshalJSON("ts.Engine.Render", "serialise module entry", entryPoint) + if err != nil { + return "", err + } + outputJSON, err := marshalJSON("ts.Engine.Render", "serialise output path", outputPath) + if err != nil { + return "", err + } + + return core.Concat( + "// Generated by dappco.re/go/html/engine/ts.\n", + "export async function init() {\n", + " const module = await import(", entryJSON, ");\n", + " let render = module.render;\n", + " if (typeof render !== \"function\" && typeof module.default === \"function\") render = module.default;\n", + " if (typeof render !== \"function\" && module.default && typeof module.default.render === \"function\") render = module.default.render.bind(module.default);\n", + " if (typeof render !== \"function\") throw new Error(\"render entry must export render(input) or a default renderer\");\n", + " const value = await render(", inputJSON, ");\n", + " const encoder = new TextEncoder();\n", + " let output;\n", + " if (typeof value === \"string\") output = encoder.encode(value);\n", + " else if (value instanceof Uint8Array) output = value;\n", + " else if (value instanceof ArrayBuffer) output = new Uint8Array(value);\n", + " else if (typeof Blob !== \"undefined\" && value instanceof Blob) output = new Uint8Array(await value.arrayBuffer());\n", + " else if (typeof Response !== \"undefined\" && value instanceof Response) output = new Uint8Array(await value.arrayBuffer());\n", + " else if (value === undefined || value === null) output = new Uint8Array();\n", + " else output = encoder.encode(JSON.stringify(value));\n", + " await Deno.writeFile(", outputJSON, ", output, { create: true, createNew: true, write: true });\n", + "}\n", + ), nil +} + +func marshalJSON(operation, message string, value any) (string, error) { + result := core.JSONMarshal(value) + if !result.OK { + return "", core.E(operation, message, resultError(result)) + } + return core.AsString(result.Value.([]byte)), nil +} + +func fileURL(path string) string { + return (&core.URL{Scheme: "file", Path: core.PathToSlash(path)}).String() +} + +func canonicalPath(path string) string { + if path == "" { + return "" + } + absoluteResult := core.PathAbs(path) + if !absoluteResult.OK { + return path + } + absolute := absoluteResult.Value.(string) + resolvedResult := core.PathEvalSymlinks(absolute) + if resolvedResult.OK { + return resolvedResult.Value.(string) + } + return absolute +} + +func resultError(result core.Result) error { + if err, ok := result.Value.(error); ok { + return err + } + return core.E("ts", result.Error(), nil) +} diff --git a/go/engine/ts/engine_example_test.go b/go/engine/ts/engine_example_test.go new file mode 100644 index 0000000..a45377b --- /dev/null +++ b/go/engine/ts/engine_example_test.go @@ -0,0 +1,17 @@ +//go:build !js + +// SPDX-Licence-Identifier: EUPL-1.2 + +package ts + +func ExampleNew() { + _ = New +} + +func ExampleEngine_Render() { + _ = (*Engine).Render +} + +func ExampleEngine_Close() { + _ = (*Engine).Close +} diff --git a/go/engine/ts/engine_test.go b/go/engine/ts/engine_test.go new file mode 100644 index 0000000..56eb47e --- /dev/null +++ b/go/engine/ts/engine_test.go @@ -0,0 +1,256 @@ +//go:build !js + +// SPDX-Licence-Identifier: EUPL-1.2 + +package ts + +import ( + "testing" + + core "dappco.re/go" + corets "dappco.re/go/ts" +) + +func TestEngine_New_Good(t *testing.T) { + engine, err := New(smokeOptions(t)) + core.RequireNoError(t, err) + core.AssertNotNil(t, engine) + core.AssertNotNil(t, engine.service) + core.AssertTrue(t, engine.service.Sidecar().IsRunning()) + core.AssertNoError(t, engine.Close()) +} + +func TestEngine_New_Bad(t *testing.T) { + engine, err := New(corets.Options{}) + core.AssertNil(t, engine) + core.AssertError(t, err) + core.AssertContains(t, err.Error(), "SidecarArgs") +} + +func TestEngine_New_Ugly(t *testing.T) { + engine, err := New(corets.Options{ + DenoPath: core.PathJoin(t.TempDir(), "missing-deno"), + SidecarArgs: []string{"run", "runtime.ts"}, + }) + core.AssertNil(t, engine) + core.AssertError(t, err) + core.AssertContains(t, err.Error(), "start CoreTS service") +} + +func TestEngine_Render_Good(t *testing.T) { + engine := newSmokeEngine(t) + defer func() { core.AssertNoError(t, engine.Close()) }() + + entry := inlineModule(`export function render(input: {name: string}) { + return "
Hello, " + input.name + "
"; + }`) + output, err := engine.Render(core.Background(), entry, map[string]any{"name": "Deno"}) + core.AssertNoError(t, err) + core.AssertEqual(t, "
Hello, Deno
", core.AsString(output)) +} + +func TestEngine_Render_Bad(t *testing.T) { + engine := &Engine{closed: true} + output, err := engine.Render(core.Background(), "entry.ts", nil) + core.AssertNil(t, output) + core.AssertError(t, err) + core.AssertContains(t, err.Error(), "closed") +} + +func TestEngine_Render_Ugly(t *testing.T) { + engine := newSmokeEngine(t) + defer func() { core.AssertNoError(t, engine.Close()) }() + + entry := inlineModule(`export default function() { + return new Uint8Array([0, 1, 2, 255]); + }`) + output, err := engine.Render(core.Background(), entry, nil) + core.AssertNoError(t, err) + core.AssertEqual(t, []byte{0, 1, 2, 255}, output) +} + +func TestEngine_Close_Good(t *testing.T) { + engine := newSmokeEngine(t) + sidecar := engine.service.Sidecar() + core.AssertTrue(t, sidecar.IsRunning()) + core.AssertNoError(t, engine.Close()) + core.AssertFalse(t, sidecar.IsRunning()) +} + +func TestEngine_Close_Bad(t *testing.T) { + var engine *Engine + err := engine.Close() + core.AssertError(t, err) + core.AssertContains(t, err.Error(), "engine is nil") + core.AssertContains(t, err.Error(), "Engine.Close") +} + +func TestEngine_Close_Ugly(t *testing.T) { + engine := newSmokeEngine(t) + core.AssertNoError(t, engine.Close()) + core.AssertNoError(t, engine.Close()) + core.AssertTrue(t, engine.closed) +} + +func newSmokeEngine(t *testing.T) *Engine { + t.Helper() + + engine, err := New(smokeOptions(t)) + core.RequireNoError(t, err) + return engine +} + +func smokeOptions(t *testing.T) corets.Options { + t.Helper() + + denoPath := findDeno(t) + tempDir := t.TempDir() + runtimePath := core.PathJoin(tempDir, "runtime.ts") + writeResult := core.WriteFile(runtimePath, core.AsBytes(smokeRuntime), 0o600) + core.RequireTrue(t, writeResult.OK, writeResult.Error()) + + return corets.Options{ + DenoPath: denoPath, + SocketPath: core.PathJoin(tempDir, "core.sock"), + DenoSocketPath: core.PathJoin(tempDir, "deno.sock"), + StoreDBPath: ":memory:", + SidecarArgs: []string{ + "run", + "-A", + "--unstable-worker-options", + runtimePath, + }, + } +} + +func findDeno(t *testing.T) string { + t.Helper() + + directories := core.Split(core.Env("PATH"), string(core.PathListSeparator)) + if home := core.Env("DIR_HOME"); home != "" { + directories = append(directories, core.PathJoin(home, ".deno", "bin")) + } + for _, directory := range directories { + for _, name := range []string{"deno", "deno.exe"} { + candidate := core.PathJoin(directory, name) + if core.Stat(candidate).OK { + return candidate + } + } + } + t.Skip("deno not installed") + return "" +} + +func inlineModule(source string) string { + return "data:application/typescript;base64," + core.Base64Encode(core.AsBytes(source)) +} + +const smokeRuntime = ` +const socketPath = Deno.env.get("DENO_SOCKET"); +if (!socketPath) throw new Error("DENO_SOCKET is required"); +try { await Deno.remove(socketPath); } catch (error) { + if (!(error instanceof Deno.errors.NotFound)) throw error; +} + +const listener = Deno.listen({ transport: "unix", path: socketPath }); +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +for await (const connection of listener) { + void serve(connection); +} + +async function serve(connection: Deno.Conn) { + let pending = ""; + const buffer = new Uint8Array(64 * 1024); + try { + for (;;) { + const count = await connection.read(buffer); + if (count === null) return; + pending += decoder.decode(buffer.subarray(0, count), { stream: true }); + for (;;) { + const newline = pending.indexOf("\n"); + if (newline < 0) break; + const line = pending.slice(0, newline); + pending = pending.slice(newline + 1); + if (line.length === 0) continue; + const request = JSON.parse(line); + const result = await dispatch(request); + await connection.write(encoder.encode(JSON.stringify({ + jsonrpc: "2.0", + id: request.id, + result, + }) + "\n")); + } + } + } finally { + connection.close(); + } +} + +async function dispatch(request: Record) { + switch (request.method) { + case "Ping": + return { ok: true }; + case "LoadModule": + return await loadModule( + String(request.code ?? ""), + String(request.entry_point ?? ""), + (request.permissions ?? {}) as Record, + ); + case "UnloadModule": + return { ok: true }; + case "ModuleStatus": + return { code: String(request.code ?? ""), status: "RUNNING" }; + default: + throw new Error("unknown method: " + request.method); + } +} + +async function loadModule( + code: string, + entryPoint: string, + permissions: Record, +) { + const workerSource = ` + "`" + ` +self.onmessage = async (event) => { + try { + const module = await import(event.data); + if (typeof module.init === "function") await module.init({}); + self.postMessage({ ok: true }); + } catch (error) { + self.postMessage({ + ok: false, + error: error instanceof Error ? error.message : String(error), + }); + } +}; +` + "`" + `; + const workerURL = "data:application/javascript;base64," + btoa(workerSource); + const worker = new Worker(workerURL, { + type: "module", + name: code, + deno: { + permissions: { + read: permissions.read ?? [], + write: permissions.write ?? [], + net: permissions.net ?? [], + run: permissions.run ?? [], + env: false, + sys: false, + ffi: false, + }, + }, + }); + try { + return await new Promise((resolve) => { + worker.onmessage = (event) => resolve(event.data); + worker.onerror = (event) => resolve({ ok: false, error: event.message }); + worker.postMessage(entryPoint); + }); + } finally { + worker.terminate(); + } +} +` diff --git a/go/go.mod b/go/go.mod index 0fd00f3..362975c 100644 --- a/go/go.mod +++ b/go/go.mod @@ -10,7 +10,7 @@ require ( charm.land/lipgloss/v2 v2.0.5 dappco.re/go/i18n v0.12.1 dappco.re/go/io v0.15.1 - dappco.re/go/process v0.16.1 + dappco.re/go/ts v0.2.2 github.com/charmbracelet/colorprofile v0.4.3 github.com/charmbracelet/harmonica v0.2.0 github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7 @@ -27,7 +27,9 @@ require ( require ( dappco.re/go/log v0.13.1 // indirect + dappco.re/go/scm v0.22.0 // indirect github.com/alecthomas/chroma/v2 v2.20.0 // indirect + github.com/apache/arrow-go/v18 v18.5.1 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-udiff v0.4.1 // indirect @@ -51,17 +53,28 @@ require ( github.com/creack/pty v1.1.24 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/duckdb/duckdb-go-bindings v0.10504.0 // indirect + github.com/duckdb/duckdb-go-bindings/lib/darwin-amd64 v0.10504.0 // indirect + github.com/duckdb/duckdb-go-bindings/lib/darwin-arm64 v0.10504.0 // indirect + github.com/duckdb/duckdb-go-bindings/lib/linux-amd64 v0.10504.0 // indirect + github.com/duckdb/duckdb-go-bindings/lib/linux-arm64 v0.10504.0 // indirect + github.com/duckdb/duckdb-go-bindings/lib/windows-amd64 v0.10504.0 // indirect + github.com/duckdb/duckdb-go/v2 v2.10504.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/goccy/go-json v0.10.6 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/google/flatbuffers v25.12.19+incompatible // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -72,24 +85,32 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pierrec/lz4/v4 v4.1.25 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sahilm/fuzzy v0.1.3 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yuin/goldmark v1.7.13 // indirect github.com/yuin/goldmark-emoji v1.0.6 // indirect + github.com/zeebo/xxh3 v1.1.0 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect golang.org/x/arch v0.25.0 // indirect - golang.org/x/crypto v0.50.0 // indirect + golang.org/x/crypto v0.53.0 // indirect + golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a // indirect golang.org/x/image v0.44.0 // indirect - golang.org/x/net v0.53.0 // indirect + golang.org/x/mod v0.37.0 // indirect + golang.org/x/net v0.56.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.46.0 // indirect + golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 // indirect + golang.org/x/tools v0.47.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 // indirect + google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go/go.sum b/go/go.sum index 9a685f6..8981a48 100644 --- a/go/go.sum +++ b/go/go.sum @@ -16,8 +16,10 @@ dappco.re/go/io v0.15.1 h1:HLrUG+uGVQgLfxeJRwMvf8VhTWe2CnAJWM4RBvs8HHo= dappco.re/go/io v0.15.1/go.mod h1:I2kYHxtgactjcyih9TYQADA8jLE3NjfZHbwCVwQ3KyE= dappco.re/go/log v0.13.1 h1:sfrB1hrxODsCo0A8psIvBqDkQD2mpoQ8S5S72zO2KlI= dappco.re/go/log v0.13.1/go.mod h1:FBMMEdmIvbXfvv3qOdW6acFnwMFgTw56csevEIELi/c= -dappco.re/go/process v0.16.1 h1:CMsiDrY7cMKd8B9UtUh0WIG9FkuNJ2wlz2x80KS20p4= -dappco.re/go/process v0.16.1/go.mod h1:GgES0/Y75iRdrxbWizxyipf7fdz4W4LB+2Qp5Yu9Owk= +dappco.re/go/scm v0.22.0 h1:V8K2T5uoIAcUoW9tZuJGlTtPBsV0rL9KwGtgspfsf84= +dappco.re/go/scm v0.22.0/go.mod h1:v1y7kFJIOHwAJctm5OZ0aHjCX4Xz1E8YseUWShtVW2g= +dappco.re/go/ts v0.2.2 h1:r+Lc+f3hCLBERFNidGsfO1K1GAbFjHrOJK4FTeOr1DA= +dappco.re/go/ts v0.2.2/go.mod h1:YEV+/a09EKTOxRfx70++NYO0J4oaFN6C9MKWEqQvdqg= forge.lthn.ai/Snider/Borg v0.3.1 h1:gfC1ZTpLoZai07oOWJiVeQ8+qJYK8A795tgVGJHbVL8= forge.lthn.ai/Snider/Borg v0.3.1/go.mod h1:Z7DJD0yHXsxSyM7Mjl6/g4gH1NBsIz44Bf5AFlV76Wg= forge.lthn.ai/Snider/Enchantrix v0.0.4 h1:biwpix/bdedfyc0iVeK15awhhJKH6TEMYOTXzHXx5TI= @@ -32,6 +34,12 @@ github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NT github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA= github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg= github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/apache/arrow-go/v18 v18.5.1 h1:yaQ6zxMGgf9YCYw4/oaeOU3AULySDlAYDOcnr4LdHdI= +github.com/apache/arrow-go/v18 v18.5.1/go.mod h1:OCCJsmdq8AsRm8FkBSSmYTwL/s4zHW9CqxeBxEytkNE= +github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc= +github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= @@ -70,6 +78,8 @@ github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiD github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +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/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= @@ -120,6 +130,20 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/duckdb/duckdb-go-bindings v0.10504.0 h1:XKOXNRetaJnMvMIDqi6etZOmh/nlEHM7saaC5UxU17I= +github.com/duckdb/duckdb-go-bindings v0.10504.0/go.mod h1:M2eB9+zGq+O4opimtLL5nWwkp8vdJQJNbFW5HKdqe4U= +github.com/duckdb/duckdb-go-bindings/lib/darwin-amd64 v0.10504.0 h1:hnWJ9SociR98hpypZPcgB+hTuWgw0hYsYnPFr8mBBEw= +github.com/duckdb/duckdb-go-bindings/lib/darwin-amd64 v0.10504.0/go.mod h1:EnAvZh1kNJHp5yF+M1ZHNEvapnmt6anq1xXHVrAGqMo= +github.com/duckdb/duckdb-go-bindings/lib/darwin-arm64 v0.10504.0 h1:q3JEdS5hU8ytvmcsgFEfGGQeJ5mS8l8QFYpfUlPHDRY= +github.com/duckdb/duckdb-go-bindings/lib/darwin-arm64 v0.10504.0/go.mod h1:IGLSeEcFhNeZF16aVjQCULD7TsFZKG5G7SyKJAXKp5c= +github.com/duckdb/duckdb-go-bindings/lib/linux-amd64 v0.10504.0 h1:2gABzYp2KnclSpE4qqinTZYhGpWSxmNHQKjs9WZ7zWk= +github.com/duckdb/duckdb-go-bindings/lib/linux-amd64 v0.10504.0/go.mod h1:KAIynZ0GHCS7X5fRyuFnQMg/SZBPK/bS9OCOVojClxw= +github.com/duckdb/duckdb-go-bindings/lib/linux-arm64 v0.10504.0 h1:iTFkIVt6Ohd/rTvNQomtbP/IIh90TFGSlOUqFEC68Xo= +github.com/duckdb/duckdb-go-bindings/lib/linux-arm64 v0.10504.0/go.mod h1:81SGOYoEUs8qaAfSk1wRfM5oobrIJ5KI7AzYhK6/bvQ= +github.com/duckdb/duckdb-go-bindings/lib/windows-amd64 v0.10504.0 h1:8O774uudYeexLa+uHekujkT3t3sDjQ7LSnlKIhRNWyc= +github.com/duckdb/duckdb-go-bindings/lib/windows-amd64 v0.10504.0/go.mod h1:K25pJL26ARblGDeuAkrdblFvUen92+CwksLtPEHRqqQ= +github.com/duckdb/duckdb-go/v2 v2.10504.0 h1:bnkcNQpz3EaJmMmfOhFRFJ3ELVDHW4u3PDhZ78ZTCMY= +github.com/duckdb/duckdb-go/v2 v2.10504.0/go.mod h1:DQ8TxrUb0RGyTTFTwPNlpQ9FFx+ocJjoZGDkuOktYp8= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= @@ -128,6 +152,10 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +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-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -136,21 +164,35 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 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/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs= +github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 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/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= @@ -171,6 +213,10 @@ github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/a github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -184,6 +230,8 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 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/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0= +github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -220,27 +268,57 @@ github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +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/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE= golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +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-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o= golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I= golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +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.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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/telemetry v0.0.0-20260625142307-59b4966ccb57 h1:nwGZBCt+FnXUrGsj5vjzAsEmkcaFvd82BbOjECiFYZc= +golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 h1:aJmi6DVGGIStN9Mobk/tZOOQUBbj0BPjZjjnOdoZKts= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=