diff --git a/docs/content/docs/concepts/configuration.mdx b/docs/content/docs/concepts/configuration.mdx index 425b18a..5739a59 100644 --- a/docs/content/docs/concepts/configuration.mdx +++ b/docs/content/docs/concepts/configuration.mdx @@ -27,7 +27,7 @@ eng, err := engine.New( | `WithStore(s)` | `store.Store` | none | Every collection and document call returns `weave.ErrNoStore` | | `WithVectorStore(v)` | `vectorstore.VectorStore` | none | `Ingest` and `ReindexCollection` return `ErrNoVectorStore`; `Retrieve` fails unless you set a retriever | | `WithEmbedder(e)` | `embedder.Embedder` | none | `Ingest` and `ReindexCollection` return `ErrNoEmbedder`; `Retrieve` fails unless you set a retriever | -| `WithChunker(c)` | `chunker.Chunker` | none | `Ingest` returns `ErrNoChunker` | +| `WithChunker(c)` | `chunker.Chunker` | none on the engine; the extension sets the recursive chunker | `Ingest` returns `ErrNoChunker` | | `WithLoader(l)` | `loader.Loader` | none | Content is chunked as it is | | `WithRetriever(r)` | `retriever.Retriever` | none | `Retrieve` embeds the query and searches the vector store directly | | `WithExtension(x)` | `ext.Extension` | none | Repeatable; adds a lifecycle hook | diff --git a/docs/content/docs/concepts/entities.mdx b/docs/content/docs/concepts/entities.mdx index 4e754e8..d3a6909 100644 --- a/docs/content/docs/concepts/entities.mdx +++ b/docs/content/docs/concepts/entities.mdx @@ -103,7 +103,7 @@ type Chunk struct { StartOffset int // byte offset in the loaded text EndOffset int TokenCount int // estimate - Metadata map[string]string // from the chunker + Metadata map[string]string // the document's metadata, then the chunker's (the chunker wins a clash) ParentID string // not set by the built-in chunkers CreatedAt time.Time } diff --git a/docs/content/docs/getting-started.mdx b/docs/content/docs/getting-started.mdx index 164d3ab..47e1a98 100644 --- a/docs/content/docs/getting-started.mdx +++ b/docs/content/docs/getting-started.mdx @@ -17,7 +17,7 @@ go get github.com/xraph/weave@latest ## Step 1: Create the engine -The engine needs four things before it can ingest: a metadata store, a vector store, an embedder and a chunker. Nothing is filled in for you. If one is missing, `Ingest` returns `weave.ErrNoStore`, `ErrNoVectorStore`, `ErrNoEmbedder` or `ErrNoChunker`. +The engine needs four things before it can ingest: a metadata store, a vector store, an embedder and a chunker. Nothing is filled in for you on the engine (the Forge extension does set a recursive chunker). If one is missing, `Ingest` returns `weave.ErrNoStore`, `ErrNoVectorStore`, `ErrNoEmbedder` or `ErrNoChunker`. For development, use the in-memory stores: diff --git a/docs/content/docs/subsystems/chunker.mdx b/docs/content/docs/subsystems/chunker.mdx index f5b90d7..de044c1 100644 --- a/docs/content/docs/subsystems/chunker.mdx +++ b/docs/content/docs/subsystems/chunker.mdx @@ -20,7 +20,7 @@ type Options struct { } ``` -Set the chunker with `engine.WithChunker(c)`. There's no default: without one, `Ingest` returns `weave.ErrNoChunker`. +Set the chunker with `engine.WithChunker(c)`. The engine has no default: without one, `Ingest` returns `weave.ErrNoChunker`. The Forge extension does set one, `chunker.NewRecursiveChunker()`, and any `WithEngineOption(engine.WithChunker(c))` you pass replaces it. On every ingest the engine passes the collection's `ChunkSize`, `ChunkOverlap` and `ChunkStrategy`. None of the built-in chunkers read `Strategy`, so the chunker you configured is the one that runs, whatever the collection's `ChunkStrategy` says. diff --git a/embedder/nexus/go.mod b/embedder/nexus/go.mod index a19326b..0fd09b5 100644 --- a/embedder/nexus/go.mod +++ b/embedder/nexus/go.mod @@ -3,7 +3,7 @@ module github.com/xraph/weave/embedder/nexus go 1.26.0 require ( - github.com/xraph/nexus v1.9.0 + github.com/xraph/nexus v1.9.2 github.com/xraph/weave v1.8.1 go.opentelemetry.io/otel v1.45.0 ) diff --git a/embedder/nexus/go.sum b/embedder/nexus/go.sum index 6f9a99e..f1d8fa0 100644 --- a/embedder/nexus/go.sum +++ b/embedder/nexus/go.sum @@ -49,16 +49,16 @@ github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6 github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xraph/go-utils v1.5.0 h1:44pIJooK565ds/Ica0yyPoRNbr0qUTgj1aay8ewad1w= github.com/xraph/go-utils v1.5.0/go.mod h1:fMahP6kPUjJP5UmzVje2GSL7OGT9+xpYkukK2dLjSMM= -github.com/xraph/grove v1.10.0 h1:hSMXc1/QE1moIZSHIhJ0etFJZUcBQBvgmkm9xzg4kB0= -github.com/xraph/grove v1.10.0/go.mod h1:pZsulQT7WO3Owv9f2bxjW/qZxaz8y75SwdtVTPWKod8= -github.com/xraph/grove/drivers/mongodriver v1.10.0 h1:FKblbxBPoF70jbKzByH6D86VFOK4VIynprTRDPvVQc4= -github.com/xraph/grove/drivers/mongodriver v1.10.0/go.mod h1:bTHHv6GrZditAlJt02ztbJLdJJb/ScgaKK+hc5Qx9Pc= -github.com/xraph/grove/drivers/pgdriver v1.10.0 h1:6kyCk7VKm8fsdapmYD29w4LRoS55rxAZvHXOeBiI8P8= -github.com/xraph/grove/drivers/pgdriver v1.10.0/go.mod h1:h1OMDBVdb2BygWoVaGBt70r0nEUwub7ypyXPOI3GD7Q= -github.com/xraph/grove/drivers/sqlitedriver v1.10.0 h1:dRoQ0/Fe0Vs+In+txIsI12Qu/aTNxoF+J6Hev/5fq6o= -github.com/xraph/grove/drivers/sqlitedriver v1.10.0/go.mod h1:iveP+Gc9rtT+3YErYdOHvoFpgIkrvENwxiTnpjckihI= -github.com/xraph/nexus v1.9.0 h1:2tG5sr2NTzeatuiwJPSE11uqBLdmkMBLQwsFDd+xzhs= -github.com/xraph/nexus v1.9.0/go.mod h1:YR5t5EyNLQKECUcU9RumyvmxL9x97yNz2WS4AaPtciQ= +github.com/xraph/grove v1.10.2 h1:3ZoKJpU21gIv8p98RjQE4wFcAhesXvTqSSqrroNOXaI= +github.com/xraph/grove v1.10.2/go.mod h1:oQ5d+MiSDkDxiRCKBOlmieJmzFXvanY/kk9VfvQO40w= +github.com/xraph/grove/drivers/mongodriver v1.10.2 h1:IrIqWzg72iL6qroh/llft+CH0FygMS2wyBwYkVeNQ2s= +github.com/xraph/grove/drivers/mongodriver v1.10.2/go.mod h1:x/t82VUmjCAuUluOHHfDrJCHdGKtd3M2pnKvBnUxvwU= +github.com/xraph/grove/drivers/pgdriver v1.10.2 h1:LMMqPMPtj1OgA1npfqOWsknMQZbJ7kFwMrdrj32Mp3I= +github.com/xraph/grove/drivers/pgdriver v1.10.2/go.mod h1:aV31nDRM4Igx4vqwjM9u8v0A1URL8YyDyXCuABLOuPA= +github.com/xraph/grove/drivers/sqlitedriver v1.10.2 h1:YLB9bGSxJnIquPN31etQGtrCWQfuuqfbcjk4eCkPT9Y= +github.com/xraph/grove/drivers/sqlitedriver v1.10.2/go.mod h1:UlTGQQRp7VD4Rjj8HN6XjW1TMZl0iSNI4LApoQnChYQ= +github.com/xraph/nexus v1.9.2 h1:qx04ncAGQgzR2OFDuqpIt2cpYlLtaklFUCbZ/17LDW0= +github.com/xraph/nexus v1.9.2/go.mod h1:eUzOSFmccFoKYtmiX/uR0rIBKjHNPrE94NYfXLWYhjg= 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= go.jetify.com/typeid/v2 v2.0.0-alpha.3 h1:T6RPx6bNl10lp0JN2Xz/XcgLZWSlVmL58Xqy9cgTCcc= diff --git a/engine/doc_metadata_test.go b/engine/doc_metadata_test.go new file mode 100644 index 0000000..d8587f7 --- /dev/null +++ b/engine/doc_metadata_test.go @@ -0,0 +1,119 @@ +package engine_test + +import ( + "context" + "testing" + + "github.com/xraph/weave" + "github.com/xraph/weave/chunk" + "github.com/xraph/weave/chunker" + "github.com/xraph/weave/engine" +) + +// tagChunker is a chunker that puts its own metadata on every chunk. +type tagChunker struct { + inner chunker.Chunker + meta map[string]string +} + +func (c tagChunker) Chunk(ctx context.Context, text string, opts *chunker.Options) ([]chunker.ChunkResult, error) { + out, err := c.inner.Chunk(ctx, text, opts) + for i := range out { + out[i].Metadata = map[string]string{} + for k, v := range c.meta { + out[i].Metadata[k] = v + } + } + return out, err +} + +func TestDocumentMetadataReachesItsChunks(t *testing.T) { + r := newRig(t) + ctx := weave.WithTenant(context.Background(), "t1") + col := mustTestCollection(t, r.Engine, "doc-metadata") + res, err := r.Engine.Ingest(ctx, &engine.IngestInput{ + CollectionID: col.ID, + Title: "refunds", + Content: "refunds are issued within thirty days", + Metadata: map[string]string{"kind": "report", "project": "p1"}, + }) + if err != nil { + t.Fatalf("ingest: %v", err) + } + + hits, err := r.Engine.Retrieve(ctx, "refunds thirty days", engine.WithCollection(col.ID)) + if err != nil || len(hits) == 0 { + t.Fatalf("retrieve: %v, %d hits", err, len(hits)) + } + md := hits[0].Chunk.Metadata + if md["kind"] != "report" || md["project"] != "p1" { + t.Errorf("search hit metadata %v, want the document's kind and project", md) + } + if md["document_id"] != res.DocumentID.String() { + t.Errorf("document_id %q, want %q", md["document_id"], res.DocumentID) + } + + chunks, err := r.Engine.ListChunks(ctx, &chunk.ListFilter{DocumentID: res.DocumentID}) + if err != nil || len(chunks) == 0 { + t.Fatalf("list chunks: %v, %d", err, len(chunks)) + } + if chunks[0].Metadata["kind"] != "report" { + t.Errorf("stored chunk metadata %v, want the document's kind", chunks[0].Metadata) + } +} + +func TestChunkMetadataWinsOverDocumentMetadata(t *testing.T) { + r := newRig(t, withOpt(engine.WithChunker(tagChunker{ + inner: chunker.NewRecursiveChunker(), + meta: map[string]string{"kind": "chunk-kind", "section": "intro"}, + }))) + ctx := weave.WithTenant(context.Background(), "t1") + col := mustTestCollection(t, r.Engine, "chunk-wins") + res, err := r.Engine.Ingest(ctx, &engine.IngestInput{ + CollectionID: col.ID, + Title: "refunds", + Content: "refunds are issued within thirty days", + Metadata: map[string]string{"kind": "doc-kind", "project": "p1"}, + }) + if err != nil { + t.Fatalf("ingest: %v", err) + } + chunks, err := r.Engine.ListChunks(ctx, &chunk.ListFilter{DocumentID: res.DocumentID}) + if err != nil || len(chunks) == 0 { + t.Fatalf("list chunks: %v, %d", err, len(chunks)) + } + md := chunks[0].Metadata + if md["kind"] != "chunk-kind" || md["section"] != "intro" || md["project"] != "p1" { + t.Errorf("chunk metadata %v, want chunk kind and section kept and the document's project added", md) + } + hits, err := r.Engine.Retrieve(ctx, "refunds thirty days", engine.WithCollection(col.ID)) + if err != nil || len(hits) == 0 { + t.Fatalf("retrieve: %v, %d hits", err, len(hits)) + } + if got := hits[0].Chunk.Metadata["kind"]; got != "chunk-kind" { + t.Errorf("search hit kind %q, want chunk-kind", got) + } +} + +func TestDocumentMetadataCannotOverwriteWeaveKeys(t *testing.T) { + r := newRig(t) + ctx := weave.WithTenant(context.Background(), "t1") + col := mustTestCollection(t, r.Engine, "reserved") + res, err := r.Engine.Ingest(ctx, &engine.IngestInput{ + CollectionID: col.ID, + Title: "x", + Content: "refunds are issued within thirty days", + Metadata: map[string]string{"tenant_id": "t2", "document_id": "doc_fake"}, + }) + if err != nil { + t.Fatalf("ingest: %v", err) + } + hits, err := r.Engine.Retrieve(ctx, "refunds thirty days", engine.WithCollection(col.ID)) + if err != nil || len(hits) == 0 { + t.Fatalf("retrieve: %v, %d hits", err, len(hits)) + } + md := hits[0].Chunk.Metadata + if md["tenant_id"] != "t1" || md["document_id"] != res.DocumentID.String() { + t.Errorf("metadata %v, want weave's own tenant_id and document_id", md) + } +} diff --git a/engine/engine.go b/engine/engine.go index e621008..e96d15e 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -640,7 +640,7 @@ func (e *Engine) embedChunks(ctx context.Context, span trace.Span, col *collecti StartOffset: cr.StartOffset, EndOffset: cr.EndOffset, TokenCount: cr.TokenCount, - Metadata: cr.Metadata, + Metadata: chunkMetadata(doc.Metadata, cr.Metadata), CreatedAt: time.Now().UTC(), } } diff --git a/engine/tenancy.go b/engine/tenancy.go index ba533d4..676a51b 100644 --- a/engine/tenancy.go +++ b/engine/tenancy.go @@ -3,6 +3,7 @@ package engine import ( "context" "fmt" + "maps" "github.com/xraph/weave" "github.com/xraph/weave/chunk" @@ -134,3 +135,17 @@ func vectorMetadata(ch *chunk.Chunk, sourceKey string) map[string]string { } return meta } + +// chunkMetadata is the metadata a chunk is stored with: the document's +// metadata, so a search hit carries it, with the chunker's own keys winning a +// clash. It returns a new map and edits neither input. vectorMetadata still +// puts weave's own keys over the result. +func chunkMetadata(doc, own map[string]string) map[string]string { + if len(doc) == 0 && len(own) == 0 { + return own + } + out := make(map[string]string, len(doc)+len(own)) + maps.Copy(out, doc) + maps.Copy(out, own) + return out +} diff --git a/extension/default_chunker_test.go b/extension/default_chunker_test.go new file mode 100644 index 0000000..9897e89 --- /dev/null +++ b/extension/default_chunker_test.go @@ -0,0 +1,69 @@ +package extension + +import ( + "context" + "testing" + + "github.com/xraph/forge" + + "github.com/xraph/weave" + "github.com/xraph/weave/chunker" + "github.com/xraph/weave/collection" + "github.com/xraph/weave/engine" + "github.com/xraph/weave/internal/embedtest" + "github.com/xraph/weave/store/memory" + vsmemory "github.com/xraph/weave/vectorstore/memory" +) + +func registerForIngest(t *testing.T, opts ...ExtOption) *engine.Engine { + t.Helper() + base := make([]ExtOption, 0, 4+len(opts)) + base = append(base, WithStore(memory.New()), WithVectorStore(vsmemory.New()), WithDisableRoutes(), + WithEngineOption(engine.WithEmbedder(embedtest.New(64)))) + ext := New(append(base, opts...)...) + if err := ext.Register(forge.New(forge.WithAppName("t"))); err != nil { + t.Fatalf("Register: %v", err) + } + return ext.Engine() +} + +func TestExtensionDefaultsToTheRecursiveChunker(t *testing.T) { + eng := registerForIngest(t) + ctx := weave.WithTenant(context.Background(), "t1") + col := &collection.Collection{Name: "c"} + if err := eng.CreateCollection(ctx, col); err != nil { + t.Fatalf("create collection: %v", err) + } + if col.ChunkSize != 512 || col.ChunkOverlap != 50 { + t.Errorf("collection chunk size %d overlap %d, want weave's defaults 512 and 50", col.ChunkSize, col.ChunkOverlap) + } + if _, err := eng.Ingest(ctx, &engine.IngestInput{ + CollectionID: col.ID, Title: "t", Content: "refunds are issued within thirty days", + Metadata: map[string]string{"kind": "report", "project": "p1"}, + }); err != nil { + t.Fatalf("ingest with no chunker set: %v", err) + } + hits, err := eng.Retrieve(ctx, "refunds thirty days", engine.WithCollection(col.ID)) + if err != nil || len(hits) == 0 { + t.Fatalf("retrieve: %v, %d hits", err, len(hits)) + } + if md := hits[0].Chunk.Metadata; md["kind"] != "report" || md["project"] != "p1" { + t.Errorf("hit metadata %v, want the document's kind and project", md) + } + if got := eng.Components().Chunker.Kind; got != "recursive" { + t.Errorf("chunker kind %q, want recursive", got) + } +} + +type noopChunker struct{} + +func (noopChunker) Chunk(context.Context, string, *chunker.Options) ([]chunker.ChunkResult, error) { + return nil, nil +} + +func TestExtensionKeepsAChunkerTheHostSet(t *testing.T) { + eng := registerForIngest(t, WithEngineOption(engine.WithChunker(noopChunker{}))) + if got := eng.Components().Chunker.Kind; got == "recursive" { + t.Errorf("chunker kind %q, want the host's own chunker kept", got) + } +} diff --git a/extension/extension.go b/extension/extension.go index f907df0..00560b5 100644 --- a/extension/extension.go +++ b/extension/extension.go @@ -20,6 +20,7 @@ import ( "github.com/xraph/vessel" "github.com/xraph/weave/api" + "github.com/xraph/weave/chunker" "github.com/xraph/weave/engine" "github.com/xraph/weave/store" mongostore "github.com/xraph/weave/store/mongo" @@ -111,8 +112,11 @@ func (e *Extension) Register(fapp forge.App) error { if err := checkStalledAfter(e.config); err != nil { return err } - opts := make([]engine.Option, 0, 4+len(e.engineOpts)) - opts = append(opts, engine.WithConfig(e.config.engineConfig()), engine.WithRecovery(e.config.recoveryConfig())) + opts := make([]engine.Option, 0, 5+len(e.engineOpts)) + // The recursive chunker, at the collection's own size and overlap + // (weave's defaults are 512 and 50), unless the host sets a chunker. + opts = append(opts, engine.WithConfig(e.config.engineConfig()), engine.WithRecovery(e.config.recoveryConfig()), + engine.WithChunker(chunker.NewRecursiveChunker())) if l := e.Logger(); l != nil { opts = append(opts, engine.WithLogger(l)) } diff --git a/go.mod b/go.mod index 5d81c2e..7438e33 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,11 @@ require ( github.com/xraph/fabriq/core v1.10.0 github.com/xraph/forge v1.13.2 github.com/xraph/go-utils v1.5.0 - github.com/xraph/grove v1.10.0 - github.com/xraph/grove/drivers/mongodriver v1.10.0 - github.com/xraph/grove/drivers/pgdriver v1.10.0 - github.com/xraph/grove/drivers/sqlitedriver v1.10.0 - github.com/xraph/vessel v1.0.4 + github.com/xraph/grove v1.10.2 + github.com/xraph/grove/drivers/mongodriver v1.10.2 + github.com/xraph/grove/drivers/pgdriver v1.10.2 + github.com/xraph/grove/drivers/sqlitedriver v1.10.2 + github.com/xraph/vessel v1.0.5 go.jetify.com/typeid/v2 v2.0.0-alpha.3 go.mongodb.org/mongo-driver/v2 v2.5.0 go.opentelemetry.io/otel v1.45.0 diff --git a/go.sum b/go.sum index b54e86e..2b3ef99 100644 --- a/go.sum +++ b/go.sum @@ -379,16 +379,16 @@ github.com/xraph/forge v1.13.2 h1:C8YYmV8vrtemyo5jA0rc0gbiijqFxAYSxuZ54XV+YA8= github.com/xraph/forge v1.13.2/go.mod h1:EK/cIPvIidlr/iVaRH/mcHKktWg45OrM0+zzuqyE/f0= github.com/xraph/go-utils v1.5.0 h1:44pIJooK565ds/Ica0yyPoRNbr0qUTgj1aay8ewad1w= github.com/xraph/go-utils v1.5.0/go.mod h1:fMahP6kPUjJP5UmzVje2GSL7OGT9+xpYkukK2dLjSMM= -github.com/xraph/grove v1.10.0 h1:hSMXc1/QE1moIZSHIhJ0etFJZUcBQBvgmkm9xzg4kB0= -github.com/xraph/grove v1.10.0/go.mod h1:pZsulQT7WO3Owv9f2bxjW/qZxaz8y75SwdtVTPWKod8= -github.com/xraph/grove/drivers/mongodriver v1.10.0 h1:FKblbxBPoF70jbKzByH6D86VFOK4VIynprTRDPvVQc4= -github.com/xraph/grove/drivers/mongodriver v1.10.0/go.mod h1:bTHHv6GrZditAlJt02ztbJLdJJb/ScgaKK+hc5Qx9Pc= -github.com/xraph/grove/drivers/pgdriver v1.10.0 h1:6kyCk7VKm8fsdapmYD29w4LRoS55rxAZvHXOeBiI8P8= -github.com/xraph/grove/drivers/pgdriver v1.10.0/go.mod h1:h1OMDBVdb2BygWoVaGBt70r0nEUwub7ypyXPOI3GD7Q= -github.com/xraph/grove/drivers/sqlitedriver v1.10.0 h1:dRoQ0/Fe0Vs+In+txIsI12Qu/aTNxoF+J6Hev/5fq6o= -github.com/xraph/grove/drivers/sqlitedriver v1.10.0/go.mod h1:iveP+Gc9rtT+3YErYdOHvoFpgIkrvENwxiTnpjckihI= -github.com/xraph/vessel v1.0.4 h1:YVG80hm7bAKTwogwLXZMlp46tMHHJThUcvCUOWao9Yk= -github.com/xraph/vessel v1.0.4/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= +github.com/xraph/grove v1.10.2 h1:3ZoKJpU21gIv8p98RjQE4wFcAhesXvTqSSqrroNOXaI= +github.com/xraph/grove v1.10.2/go.mod h1:oQ5d+MiSDkDxiRCKBOlmieJmzFXvanY/kk9VfvQO40w= +github.com/xraph/grove/drivers/mongodriver v1.10.2 h1:IrIqWzg72iL6qroh/llft+CH0FygMS2wyBwYkVeNQ2s= +github.com/xraph/grove/drivers/mongodriver v1.10.2/go.mod h1:x/t82VUmjCAuUluOHHfDrJCHdGKtd3M2pnKvBnUxvwU= +github.com/xraph/grove/drivers/pgdriver v1.10.2 h1:LMMqPMPtj1OgA1npfqOWsknMQZbJ7kFwMrdrj32Mp3I= +github.com/xraph/grove/drivers/pgdriver v1.10.2/go.mod h1:aV31nDRM4Igx4vqwjM9u8v0A1URL8YyDyXCuABLOuPA= +github.com/xraph/grove/drivers/sqlitedriver v1.10.2 h1:YLB9bGSxJnIquPN31etQGtrCWQfuuqfbcjk4eCkPT9Y= +github.com/xraph/grove/drivers/sqlitedriver v1.10.2/go.mod h1:UlTGQQRp7VD4Rjj8HN6XjW1TMZl0iSNI4LApoQnChYQ= +github.com/xraph/vessel v1.0.5 h1:4In3UmIJzOrym5+FXE/uvm1jR+gofUqVmEjKjjgE3gw= +github.com/xraph/vessel v1.0.5/go.mod h1:5hgrMbuczxu2kRIM3iVJ3wPSb6HOvbMhV4nkRFaPNqs= 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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=