From 09d34fb4d4325c8de22dab0c801b68e59cec1da7 Mon Sep 17 00:00:00 2001 From: elpresidank Date: Tue, 2 Jun 2026 09:11:33 -0500 Subject: [PATCH] Normalize term translation with Effect Match --- ts/EFFECT_NATIVE_REWRITE_AUDIT.md | 107 ++++++++-- .../base/src/__tests__/schema-effect.test.ts | 15 ++ ts/packages/base/src/schema/primitives.ts | 30 ++- .../src/__tests__/gateway-dispatcher.test.ts | 69 ++++++ ts/packages/flow/src/agent/react/tools.ts | 21 +- .../flow/src/gateway/dispatch/serialize.ts | 197 +++++++++--------- .../flow/src/query/triples/falkordb.ts | 20 +- ts/packages/flow/src/retrieval/graph-rag.ts | 21 +- .../src/storage/embeddings/qdrant-graph.ts | 20 +- .../flow/src/storage/triples/falkordb.ts | 20 +- ts/packages/workbench/src/lib/graph-utils.ts | 19 +- 11 files changed, 349 insertions(+), 190 deletions(-) diff --git a/ts/EFFECT_NATIVE_REWRITE_AUDIT.md b/ts/EFFECT_NATIVE_REWRITE_AUDIT.md index b816fc21..8bff92ac 100644 --- a/ts/EFFECT_NATIVE_REWRITE_AUDIT.md +++ b/ts/EFFECT_NATIVE_REWRITE_AUDIT.md @@ -1508,6 +1508,44 @@ Notes: - `cd ts && bun run lint` - `git diff --check` +### 2026-06-02: Term And ClientTerm Match Slice + +- Status: migrated and package-verified. +- Completed: + - `ts/packages/base/src/schema/primitives.ts` now exposes `Term` as a + recursive `S.toTaggedUnion("type")` schema and aligns `Triple.g` with the + Python/client wire contract as an optional graph string. + - `ts/packages/flow/src/gateway/dispatch/serialize.ts` now defines compact + client-term schemas with `S.tag`, decodes unknown term-shaped values with + Schema/Option, and translates terms with + `Match.discriminatorsExhaustive`. + - Removed the gateway serializer's native term switches and unsafe + pass-through casts. Malformed known-tag objects now stay ordinary payload + objects during deep translation instead of being cast into invalid terms. + - Replaced pure term helper switches in FalkorDB triples store/query, + Qdrant graph embeddings store, Graph RAG, agent tools, and workbench graph + utilities with exhaustive `Match` discriminators. + - Added tests for named graph string decoding, nested compact/internal term + round trips, and malformed known-tag payload preservation. +- Remaining: + - The client socket streaming term schema is still a local recursive union + and can be centralized later if drift appears. It has no native term + switch in the current scan. + - Operation dispatch switches in config, cores, librarian, and flow-manager + are separate service-command refactors, not part of this term wire slice. +- Verification: + - `cd ts && bun run check:tsgo` + - `cd ts/packages/base && bunx --bun vitest run src/__tests__/schema-effect.test.ts` + - `cd ts/packages/flow && bunx --bun vitest run src/__tests__/gateway-dispatcher.test.ts src/__tests__/falkordb-lifecycle.test.ts src/__tests__/qdrant-embeddings.test.ts src/__tests__/retrieval-rag.test.ts` + - `cd ts/packages/flow && bun run test` + - `cd ts/packages/base && bun run build` + - `cd ts/packages/flow && bun run build` + - `cd ts/packages/workbench && bun run build` + - `cd ts && bun run build` + - `cd ts && bun run test` + - `cd ts && bun run lint` + - `git diff --check` + ## Subagent Findings To Preserve - MCP/workbench: @@ -1616,12 +1654,19 @@ Notes: complete. The remaining provider-layer item is parity-backed Effect AI adapter work, not a direct SDK swap. - Scratch-note follow-ups: - - `Term` / compact client term serialization is the next strongest schema - migration: prefer `S.toTaggedUnion(...).match` or `Match` helpers over the - current native switches and unsafe serializer fallbacks. + - `Term` / compact client term serialization is complete for base schema, + gateway translation, and pure term helper switches. Future work should + only reopen this if client socket schema drift appears or a hidden + consumer needs a different named-graph shape. + - Messaging runtime `Config.duration` is the next strongest scratch target: + internal runtime config can use `Duration.Duration` while public + `timeoutMs` compatibility surfaces stay numeric. + - Qdrant graph/doc known-collection caches are a good small + `MutableHashSet` candidate; short-lived local traversal sets + remain no-ops. - FlowManager and sibling service `() => Effect.gen(...)` factories remain a broad mechanical `Effect.fn` / `Effect.fnUntraced` cleanup, best handled - after the term schema slice. + after Duration and small collection slices. - Long-lived `Map` / `Set` state in ref-backed services can move toward Effect collections later; static lookup tables and local pure traversal maps/sets remain no-ops. @@ -1711,7 +1756,7 @@ Notes: - Use a fresh `Metric.MetricRegistry` in tests that assert exact scrape content. -### P1: Term And ClientTerm Tagged-Union Normalization +### Complete: Term And ClientTerm Tagged-Union Normalization - TrustGraph evidence: - `ts/packages/base/src/schema/primitives.ts` @@ -1726,9 +1771,47 @@ Notes: - Remove unsafe default pass-through casts while preserving compact `g` string compatibility. - Tests: - - Extend base schema tests for recursive terms and add gateway serializer - coverage for all variants, nested triples, compact graph strings, and - malformed client triples. + - Base schema tests now cover recursive terms and graph strings. + - Gateway dispatcher tests now cover all compact term variants, nested + triples, compact graph strings, malformed known-tag payloads, and + malformed client triple failures. + +### P1: Messaging Runtime Duration Config Cleanup + +- TrustGraph evidence: + - `ts/packages/base/src/runtime/messaging-config.ts` + - `ts/packages/base/src/messaging/runtime.ts` +- Effect primitives: + - `Config.duration`, `Duration.Duration`, and existing `Duration.millis` + compatibility conversions. +- Rewrite shape: + - Change internal runtime config fields such as `receiveTimeoutMs`, + `requestTimeoutMs`, `retryDelayMs`, and `rateLimitTimeoutMs` to + `Duration.Duration`. + - Load env-backed values with `Config.duration` while preserving Python-style + millisecond defaults and public numeric compatibility options. + - Keep external `timeoutMs` option names numeric in request/response, + processor, and client boundaries unless their public API is deliberately + changed. +- Tests: + - Extend base runtime config tests for env duration parsing and verify + messaging retry/timeout behavior still uses the same effective durations. + +### P2: Qdrant Known-Collection MutableHashSet Cleanup + +- TrustGraph evidence: + - `ts/packages/flow/src/storage/embeddings/qdrant-doc.ts` + - `ts/packages/flow/src/storage/embeddings/qdrant-graph.ts` +- Effect primitives: + - `MutableHashSet` from `effect`. +- Rewrite shape: + - Replace long-lived `Set` known-collection caches with + `MutableHashSet` in Qdrant graph/doc embedding stores. + - Keep short-lived local `Set` values for pure query traversal or fixture + assertions as no-op boundaries. +- Tests: + - Existing Qdrant embeddings tests should prove lazy collection creation and + deletion cache invalidation still behave the same. ### P2: Canonicalize MCP Around The Effect Server @@ -1765,10 +1848,10 @@ Notes: ## Recommended PR Order -1. MCP protocol parity tests and legacy stdio flip/removal decision. -2. Term/ClientTerm Schema tagged-union and Match normalization. -3. FlowManager/service `Effect.fn` normalization. -4. Messaging runtime `Config.duration` / `Duration` cleanup. +1. Messaging runtime `Config.duration` / `Duration` cleanup. +2. Qdrant known-collection `MutableHashSet` cleanup. +3. MCP protocol parity tests and legacy stdio flip/removal decision. +4. FlowManager/service `Effect.fn` normalization. ## No-Op Rules diff --git a/ts/packages/base/src/__tests__/schema-effect.test.ts b/ts/packages/base/src/__tests__/schema-effect.test.ts index 8b868cbb..ceabcedd 100644 --- a/ts/packages/base/src/__tests__/schema-effect.test.ts +++ b/ts/packages/base/src/__tests__/schema-effect.test.ts @@ -6,6 +6,7 @@ import { GraphRagResponse, Term, TextCompletionRequest, + Triple, loadProcessorRuntimeConfig, } from "../index.js"; @@ -40,6 +41,20 @@ describe("Effect schemas", () => { }), ); + it.effect( + "decode triples with named graph strings", + Effect.fnUntraced(function* () { + const triple = yield* S.decodeUnknownEffect(Triple)({ + s: { type: "IRI", iri: "urn:s" }, + p: { type: "IRI", iri: "urn:p" }, + o: { type: "LITERAL", value: "object" }, + g: "urn:graph", + }); + + expect(triple.g).toBe("urn:graph"); + }), + ); + it.effect( "preserve gateway response extension fields", Effect.fnUntraced(function* () { diff --git a/ts/packages/base/src/schema/primitives.ts b/ts/packages/base/src/schema/primitives.ts index 5a42a287..21d74000 100644 --- a/ts/packages/base/src/schema/primitives.ts +++ b/ts/packages/base/src/schema/primitives.ts @@ -45,30 +45,28 @@ export type Triple = { readonly s: Term; readonly p: Term; readonly o: Term; - readonly g?: Term; + readonly g?: string; }; -export const Triple: S.Codec = S.suspend(() => - S.Struct({ - s: Term, - p: Term, - o: Term, - g: S.optionalKey(Term), - }) -); +export const Triple: S.Codec = S.Struct({ + s: S.suspend((): S.Codec => Term), + p: S.suspend((): S.Codec => Term), + o: S.suspend((): S.Codec => Term), + g: S.optionalKey(S.String), +}); -export const TripleTerm: S.Codec = S.suspend(() => - S.Struct({ - type: S.tag("TRIPLE"), - triple: Triple, - }) -); +export const TripleTerm: S.Codec = S.Struct({ + type: S.tag("TRIPLE"), + triple: S.suspend((): S.Codec => Triple), +}); export interface TripleTerm { readonly type: "TRIPLE"; readonly triple: Triple; } -export const Term: S.Codec = S.suspend(() => S.Union([IriTerm, BlankTerm, LiteralTerm, TripleTerm])); +export const Term = S.Union([IriTerm, BlankTerm, LiteralTerm, TripleTerm]).pipe( + S.toTaggedUnion("type"), +); export const Field = S.Struct({ name: S.String, diff --git a/ts/packages/flow/src/__tests__/gateway-dispatcher.test.ts b/ts/packages/flow/src/__tests__/gateway-dispatcher.test.ts index e12d1c80..454dfd63 100644 --- a/ts/packages/flow/src/__tests__/gateway-dispatcher.test.ts +++ b/ts/packages/flow/src/__tests__/gateway-dispatcher.test.ts @@ -4,6 +4,12 @@ import { dispatcherManagerIsCompleteResponse, makeDispatcherManager, } from "../gateway/dispatch/manager.js"; +import { + clientTermToInternal, + internalTermToClient, + translateRequestEffect, + translateResponseEffect, +} from "../gateway/dispatch/serialize.js"; import type { BackendConsumer, BackendProducer, @@ -153,6 +159,69 @@ class DispatchBackend implements PubSubBackend { } describe("gateway dispatcher manager", () => { + it("translates compact client terms with Match and schema-backed narrowing", async () => { + const internal = clientTermToInternal({ + t: "t", + tr: { + s: { t: "i", i: "urn:s" }, + p: { t: "b", d: "blank" }, + o: { t: "l", v: "value", dt: "urn:datatype", ln: "en" }, + g: "urn:graph", + }, + }); + + expect(internal).toEqual({ + type: "TRIPLE", + triple: { + s: { type: "IRI", iri: "urn:s" }, + p: { type: "BLANK", id: "blank" }, + o: { + type: "LITERAL", + value: "value", + datatype: "urn:datatype", + language: "en", + }, + g: "urn:graph", + }, + }); + + expect(internalTermToClient(internal)).toEqual({ + t: "t", + tr: { + s: { t: "i", i: "urn:s" }, + p: { t: "b", d: "blank" }, + o: { t: "l", v: "value", dt: "urn:datatype", ln: "en" }, + g: "urn:graph", + }, + }); + }); + + it("deep-translates only schema-valid term-shaped values", async () => { + const request = await Effect.runPromise( + translateRequestEffect("knowledge", { + term: { t: "i", i: "urn:item" }, + malformedKnownTag: { t: "i" }, + untouched: { t: "unknown", value: "kept" }, + }), + ); + const response = await Effect.runPromise( + translateResponseEffect("knowledge", { + term: { type: "IRI", iri: "urn:item" }, + untouched: { type: "unknown", value: "kept" }, + }), + ); + + expect(request).toEqual({ + term: { type: "IRI", iri: "urn:item" }, + malformedKnownTag: { t: "i" }, + untouched: { t: "unknown", value: "kept" }, + }); + expect(response).toEqual({ + term: { t: "i", i: "urn:item" }, + untouched: { type: "unknown", value: "kept" }, + }); + }); + it("caches Effect requestors as scoped handles", async () => { const backend = new DispatchBackend(); const manager = makeDispatcherManager({ diff --git a/ts/packages/flow/src/agent/react/tools.ts b/ts/packages/flow/src/agent/react/tools.ts index 8a0c1eef..271ad0ad 100644 --- a/ts/packages/flow/src/agent/react/tools.ts +++ b/ts/packages/flow/src/agent/react/tools.ts @@ -19,7 +19,7 @@ import type { Triple, } from "@trustgraph/base"; import {Term as TermSchema} from "@trustgraph/base"; -import { Effect } from "effect"; +import { Effect, Match } from "effect"; import * as O from "effect/Option"; import * as Predicate from "effect/Predicate"; import * as S from "effect/Schema"; @@ -33,16 +33,15 @@ const decodeTerm = S.decodeUnknownOption(TermSchema); * Format a Term to a human-readable string. */ function termToString(term: Term): string { - switch (term.type) { - case "IRI": - return term.iri; - case "LITERAL": - return term.value; - case "BLANK": - return `_:${term.id}`; - case "TRIPLE": - return `(${termToString(term.triple.s)} ${termToString(term.triple.p)} ${termToString(term.triple.o)})`; - } + return Match.type().pipe( + Match.discriminatorsExhaustive("type")({ + IRI: (iri) => iri.iri, + LITERAL: (literal) => literal.value, + BLANK: (blank) => `_:${blank.id}`, + TRIPLE: (triple) => + `(${termToString(triple.triple.s)} ${termToString(triple.triple.p)} ${termToString(triple.triple.o)})`, + }), + )(term); } /** diff --git a/ts/packages/flow/src/gateway/dispatch/serialize.ts b/ts/packages/flow/src/gateway/dispatch/serialize.ts index 1523dcf4..2b756988 100644 --- a/ts/packages/flow/src/gateway/dispatch/serialize.ts +++ b/ts/packages/flow/src/gateway/dispatch/serialize.ts @@ -18,8 +18,19 @@ * Python reference: trustgraph-base/trustgraph/messaging/translators/primitives.py */ -import { errorMessage, type Term, type Triple } from "@trustgraph/base"; -import { Effect } from "effect"; +import { + BlankTerm, + errorMessage, + IriTerm, + LiteralTerm, + Term as TermSchema, + Triple as TripleSchema, + TripleTerm, + type Term, + type Triple, +} from "@trustgraph/base"; +import { Effect, Match } from "effect"; +import * as O from "effect/Option"; import * as S from "effect/Schema"; // ---------- Client wire format type definitions ---------- @@ -65,77 +76,102 @@ interface ClientTriple { g?: string; } -// ---------- Client → Internal ---------- +const ClientIriTermSchema = S.Struct({ + t: S.tag("i"), + i: S.String, +}); -export function clientTermToInternal(wire: ClientTerm): Term { - switch (wire.t) { - case "i": - return { type: "IRI", iri: wire.i }; - case "b": - return { type: "BLANK", id: wire.d }; - case "l": - return { - type: "LITERAL", - value: wire.v, - ...(wire.dt !== undefined ? { datatype: wire.dt } : {}), - ...(wire.ln !== undefined ? { language: wire.ln } : {}), - }; - case "t": { +const ClientBlankTermSchema = S.Struct({ + t: S.tag("b"), + d: S.String, +}); + +const ClientLiteralTermSchema = S.Struct({ + t: S.tag("l"), + v: S.String, + dt: S.optionalKey(S.String), + ln: S.optionalKey(S.String), +}); + +const ClientTripleSchema: S.Codec = S.Struct({ + s: S.suspend((): S.Codec => ClientTermSchema), + p: S.suspend((): S.Codec => ClientTermSchema), + o: S.suspend((): S.Codec => ClientTermSchema), + g: S.optionalKey(S.String), +}); + +const ClientTripleTermSchema = S.Struct({ + t: S.tag("t"), + tr: S.optionalKey(ClientTripleSchema), +}); + +const ClientTermSchema = S.Union([ + ClientIriTermSchema, + ClientBlankTermSchema, + ClientLiteralTermSchema, + ClientTripleTermSchema, +]).pipe(S.toTaggedUnion("t")); + +const decodeClientTerm = S.decodeUnknownOption(ClientTermSchema); +const decodeInternalTerm = S.decodeUnknownOption(TermSchema); + +const clientTermToInternalMatch = Match.type().pipe( + Match.discriminatorsExhaustive("t")({ + i: (wire) => IriTerm.make({ iri: wire.i }), + b: (wire) => BlankTerm.make({ id: wire.d }), + l: (wire) => LiteralTerm.make({ + value: wire.v, + ...(wire.dt !== undefined ? { datatype: wire.dt } : {}), + ...(wire.ln !== undefined ? { language: wire.ln } : {}), + }), + t: (wire) => { if (wire.tr === undefined) { throw DispatchSerializationError.make({ operation: "client-term-to-internal", message: "Client triple term is missing tr", }); } - return { - type: "TRIPLE", + return TripleTerm.make({ triple: clientTripleToInternal(wire.tr), - }; - } - default: - // Defensive: pass through unknown term types - return wire as unknown as Term; - } + }); + }, + }), +); + +const internalTermToClientMatch = Match.type().pipe( + Match.discriminatorsExhaustive("type")({ + IRI: (term) => ClientIriTermSchema.make({ i: term.iri }), + BLANK: (term) => ClientBlankTermSchema.make({ d: term.id }), + LITERAL: (term) => ClientLiteralTermSchema.make({ + v: term.value, + ...(term.datatype !== undefined ? { dt: term.datatype } : {}), + ...(term.language !== undefined ? { ln: term.language } : {}), + }), + TRIPLE: (term) => ClientTripleTermSchema.make({ + tr: internalTripleToClient(term.triple), + }), + }), +); + +// ---------- Client → Internal ---------- + +export function clientTermToInternal(wire: ClientTerm): Term { + return clientTermToInternalMatch(wire); } export function clientTripleToInternal(wire: ClientTriple): Triple { - const result: Triple = { + return TripleSchema.make({ s: clientTermToInternal(wire.s), p: clientTermToInternal(wire.p), o: clientTermToInternal(wire.o), - }; - if (wire.g !== undefined) { - // In the client wire format, g is a plain string. - // In the internal format, g is an optional Term (named graph). - // The Python translator treats g as a plain string passthrough, - // so we keep it as-is for compatibility. - (result as unknown as Record).g = wire.g; - } - return result; + ...(wire.g !== undefined ? { g: wire.g } : {}), + }); } // ---------- Internal → Client ---------- export function internalTermToClient(term: Term): ClientTerm { - switch (term.type) { - case "IRI": - return { t: "i", i: term.iri }; - case "BLANK": - return { t: "b", d: term.id }; - case "LITERAL": { - const lit: ClientLiteralTerm = { t: "l", v: term.value }; - if (term.datatype !== undefined) lit.dt = term.datatype; - if (term.language !== undefined) lit.ln = term.language; - return lit; - } - case "TRIPLE": - return { - t: "t", - tr: internalTripleToClient(term.triple), - }; - default: - return term as unknown as ClientTerm; - } + return internalTermToClientMatch(term); } export function internalTripleToClient(triple: Triple): ClientTriple { @@ -144,17 +180,8 @@ export function internalTripleToClient(triple: Triple): ClientTriple { p: internalTermToClient(triple.p), o: internalTermToClient(triple.o), }; - const g = (triple as unknown as Record).g; - if (g !== undefined && g !== null) { - if (typeof g === "string") { - result.g = g; - } else { - // If g is a Term, convert it back to client wire format - const iri = (g as Record).iri; - if (typeof iri === "string") { - result.g = iri; - } - } + if (triple.g !== undefined) { + result.g = triple.g; } return result; } @@ -166,32 +193,6 @@ export function internalTripleToClient(triple: Triple): ClientTriple { * A client term is detected by the presence of a `t` property * with value "i", "b", "l", or "t". */ -function isClientTerm(v: unknown): v is ClientTerm { - return ( - typeof v === "object" && - v !== null && - "t" in v && - typeof (v as Record).t === "string" && - ["i", "b", "l", "t"].includes((v as Record).t as string) - ); -} - -/** - * An internal term is detected by the presence of a `type` property - * with value "IRI", "BLANK", "LITERAL", or "TRIPLE". - */ -function isInternalTerm(v: unknown): v is Term { - return ( - typeof v === "object" && - v !== null && - "type" in v && - typeof (v as Record).type === "string" && - ["IRI", "BLANK", "LITERAL", "TRIPLE"].includes( - (v as Record).type as string, - ) - ); -} - /** * Deep-translate all client Terms in a request body to internal format. * Handles nested objects and arrays. @@ -204,11 +205,12 @@ function deepClientToInternal(value: unknown): unknown { } if (typeof value === "object") { - if (isClientTerm(value)) { - return clientTermToInternal(value); + const term = decodeClientTerm(value); + if (O.isSome(term)) { + return clientTermToInternal(term.value); } const result: Record = {}; - for (const [k, v] of Object.entries(value as Record)) { + for (const [k, v] of Object.entries(value)) { result[k] = deepClientToInternal(v); } return result; @@ -229,11 +231,12 @@ function deepInternalToClient(value: unknown): unknown { } if (typeof value === "object") { - if (isInternalTerm(value)) { - return internalTermToClient(value); + const term = decodeInternalTerm(value); + if (O.isSome(term)) { + return internalTermToClient(term.value); } const result: Record = {}; - for (const [k, v] of Object.entries(value as Record)) { + for (const [k, v] of Object.entries(value)) { result[k] = deepInternalToClient(v); } return result; diff --git a/ts/packages/flow/src/query/triples/falkordb.ts b/ts/packages/flow/src/query/triples/falkordb.ts index e2ec1bd8..0ea23ca3 100644 --- a/ts/packages/flow/src/query/triples/falkordb.ts +++ b/ts/packages/flow/src/query/triples/falkordb.ts @@ -8,7 +8,7 @@ import { createClient, Graph } from "falkordb"; import { errorMessage, type Term, type Triple } from "@trustgraph/base"; -import { Config, Context, Effect, Layer } from "effect"; +import { Config, Context, Effect, Layer, Match } from "effect"; import * as Predicate from "effect/Predicate"; import * as S from "effect/Schema"; @@ -41,16 +41,14 @@ export interface FalkorDBQueryConfig { function termToValue(term: Term | undefined): string | null { if (term === undefined) return null; - switch (term.type) { - case "IRI": - return term.iri; - case "LITERAL": - return term.value; - case "BLANK": - return term.id; - default: - return null; - } + return Match.type().pipe( + Match.discriminatorsExhaustive("type")({ + IRI: (iri) => iri.iri, + LITERAL: (literal) => literal.value, + BLANK: (blank) => blank.id, + TRIPLE: () => null, + }), + )(term); } function createTerm(value: string): Term { diff --git a/ts/packages/flow/src/retrieval/graph-rag.ts b/ts/packages/flow/src/retrieval/graph-rag.ts index 3eaff23b..ea4d1b9c 100644 --- a/ts/packages/flow/src/retrieval/graph-rag.ts +++ b/ts/packages/flow/src/retrieval/graph-rag.ts @@ -21,7 +21,7 @@ import type { TriplesQueryResponse, } from "@trustgraph/base"; import { errorMessage } from "@trustgraph/base"; -import { Context, Effect, Layer } from "effect"; +import { Context, Effect, Layer, Match } from "effect"; import * as O from "effect/Option"; import * as S from "effect/Schema"; @@ -461,16 +461,15 @@ function parseScoredEdges(responseText: string): Array { } export function termToString(term: Term): string { - switch (term.type) { - case "IRI": - return term.iri; - case "LITERAL": - return term.value; - case "BLANK": - return `_:${term.id}`; - case "TRIPLE": - return `(${termToString(term.triple.s)} ${termToString(term.triple.p)} ${termToString(term.triple.o)})`; - } + return Match.type().pipe( + Match.discriminatorsExhaustive("type")({ + IRI: (iri) => iri.iri, + LITERAL: (literal) => literal.value, + BLANK: (blank) => `_:${blank.id}`, + TRIPLE: (triple) => + `(${termToString(triple.triple.s)} ${termToString(triple.triple.p)} ${termToString(triple.triple.o)})`, + }), + )(term); } export function stringToTerm(value: string): Term { diff --git a/ts/packages/flow/src/storage/embeddings/qdrant-graph.ts b/ts/packages/flow/src/storage/embeddings/qdrant-graph.ts index 0af2b79b..247283de 100644 --- a/ts/packages/flow/src/storage/embeddings/qdrant-graph.ts +++ b/ts/packages/flow/src/storage/embeddings/qdrant-graph.ts @@ -9,7 +9,7 @@ */ import { errorMessage, type Term } from "@trustgraph/base"; -import { Config, Context, Effect, Layer, Random } from "effect"; +import { Config, Context, Effect, Layer, Match, Random } from "effect"; import * as O from "effect/Option"; import * as S from "effect/Schema"; import { makeQdrantClient, type QdrantClientFactory, type QdrantClientLike } from "../../qdrant/client.js"; @@ -84,16 +84,14 @@ const randomPointId = Effect.fn("QdrantGraphEmbeddings.randomPointId")(function* }); function getTermValue(term: Term): string | null { - switch (term.type) { - case "IRI": - return term.iri; - case "LITERAL": - return term.value; - case "BLANK": - return term.id; - case "TRIPLE": - return null; - } + return Match.type().pipe( + Match.discriminatorsExhaustive("type")({ + IRI: (iri) => iri.iri, + LITERAL: (literal) => literal.value, + BLANK: (blank) => blank.id, + TRIPLE: () => null, + }), + )(term); } export interface QdrantGraphEmbeddingsStore { diff --git a/ts/packages/flow/src/storage/triples/falkordb.ts b/ts/packages/flow/src/storage/triples/falkordb.ts index 45d986f1..576b7f37 100644 --- a/ts/packages/flow/src/storage/triples/falkordb.ts +++ b/ts/packages/flow/src/storage/triples/falkordb.ts @@ -9,7 +9,7 @@ import { createClient, Graph } from "falkordb"; import { errorMessage, type Term, type Triple } from "@trustgraph/base"; -import { Config, Context, Effect, Layer } from "effect"; +import { Config, Context, Effect, Layer, Match } from "effect"; import * as S from "effect/Schema"; export interface FalkorDBClosableClient { @@ -40,16 +40,14 @@ export interface FalkorDBConfig { } function getTermValue(term: Term): string { - switch (term.type) { - case "IRI": - return term.iri; - case "LITERAL": - return term.value; - case "BLANK": - return term.id; - case "TRIPLE": - return getTermValue(term.triple.s); - } + return Match.type().pipe( + Match.discriminatorsExhaustive("type")({ + IRI: (iri) => iri.iri, + LITERAL: (literal) => literal.value, + BLANK: (blank) => blank.id, + TRIPLE: (triple) => getTermValue(triple.triple.s), + }), + )(term); } export interface FalkorDBTriplesStore { diff --git a/ts/packages/workbench/src/lib/graph-utils.ts b/ts/packages/workbench/src/lib/graph-utils.ts index e9b99307..73e8f87f 100644 --- a/ts/packages/workbench/src/lib/graph-utils.ts +++ b/ts/packages/workbench/src/lib/graph-utils.ts @@ -1,4 +1,5 @@ import type { Triple, Term } from "@trustgraph/client"; +import { Match } from "effect"; import type { NodeObject, LinkObject } from "react-force-graph-2d"; // --------------------------------------------------------------------------- @@ -36,16 +37,14 @@ export interface GraphData { // --------------------------------------------------------------------------- export function termValue(t: Term): string { - switch (t.t) { - case "i": - return t.i; - case "l": - return t.v; - case "b": - return t.d; - case "t": - return "[triple]"; - } + return Match.type().pipe( + Match.discriminatorsExhaustive("t")({ + i: (iri) => iri.i, + l: (literal) => literal.v, + b: (blank) => blank.d, + t: () => "[triple]", + }), + )(t); } export function isIri(t: Term): boolean {