mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-02 22:41:01 +02:00
Normalize term translation with Effect Match
This commit is contained in:
parent
e311315556
commit
09d34fb4d4
11 changed files with 349 additions and 190 deletions
|
|
@ -1508,6 +1508,44 @@ Notes:
|
||||||
- `cd ts && bun run lint`
|
- `cd ts && bun run lint`
|
||||||
- `git diff --check`
|
- `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
|
## Subagent Findings To Preserve
|
||||||
|
|
||||||
- MCP/workbench:
|
- MCP/workbench:
|
||||||
|
|
@ -1616,12 +1654,19 @@ Notes:
|
||||||
complete. The remaining provider-layer item is parity-backed Effect AI
|
complete. The remaining provider-layer item is parity-backed Effect AI
|
||||||
adapter work, not a direct SDK swap.
|
adapter work, not a direct SDK swap.
|
||||||
- Scratch-note follow-ups:
|
- Scratch-note follow-ups:
|
||||||
- `Term` / compact client term serialization is the next strongest schema
|
- `Term` / compact client term serialization is complete for base schema,
|
||||||
migration: prefer `S.toTaggedUnion(...).match` or `Match` helpers over the
|
gateway translation, and pure term helper switches. Future work should
|
||||||
current native switches and unsafe serializer fallbacks.
|
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<string>` candidate; short-lived local traversal sets
|
||||||
|
remain no-ops.
|
||||||
- FlowManager and sibling service `() => Effect.gen(...)` factories remain a
|
- FlowManager and sibling service `() => Effect.gen(...)` factories remain a
|
||||||
broad mechanical `Effect.fn` / `Effect.fnUntraced` cleanup, best handled
|
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
|
- Long-lived `Map` / `Set` state in ref-backed services can move toward
|
||||||
Effect collections later; static lookup tables and local pure traversal
|
Effect collections later; static lookup tables and local pure traversal
|
||||||
maps/sets remain no-ops.
|
maps/sets remain no-ops.
|
||||||
|
|
@ -1711,7 +1756,7 @@ Notes:
|
||||||
- Use a fresh `Metric.MetricRegistry` in tests that assert exact scrape
|
- Use a fresh `Metric.MetricRegistry` in tests that assert exact scrape
|
||||||
content.
|
content.
|
||||||
|
|
||||||
### P1: Term And ClientTerm Tagged-Union Normalization
|
### Complete: Term And ClientTerm Tagged-Union Normalization
|
||||||
|
|
||||||
- TrustGraph evidence:
|
- TrustGraph evidence:
|
||||||
- `ts/packages/base/src/schema/primitives.ts`
|
- `ts/packages/base/src/schema/primitives.ts`
|
||||||
|
|
@ -1726,9 +1771,47 @@ Notes:
|
||||||
- Remove unsafe default pass-through casts while preserving compact `g`
|
- Remove unsafe default pass-through casts while preserving compact `g`
|
||||||
string compatibility.
|
string compatibility.
|
||||||
- Tests:
|
- Tests:
|
||||||
- Extend base schema tests for recursive terms and add gateway serializer
|
- Base schema tests now cover recursive terms and graph strings.
|
||||||
coverage for all variants, nested triples, compact graph strings, and
|
- Gateway dispatcher tests now cover all compact term variants, nested
|
||||||
malformed client triples.
|
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<string>` known-collection caches with
|
||||||
|
`MutableHashSet<string>` 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
|
### P2: Canonicalize MCP Around The Effect Server
|
||||||
|
|
||||||
|
|
@ -1765,10 +1848,10 @@ Notes:
|
||||||
|
|
||||||
## Recommended PR Order
|
## Recommended PR Order
|
||||||
|
|
||||||
1. MCP protocol parity tests and legacy stdio flip/removal decision.
|
1. Messaging runtime `Config.duration` / `Duration` cleanup.
|
||||||
2. Term/ClientTerm Schema tagged-union and Match normalization.
|
2. Qdrant known-collection `MutableHashSet` cleanup.
|
||||||
3. FlowManager/service `Effect.fn` normalization.
|
3. MCP protocol parity tests and legacy stdio flip/removal decision.
|
||||||
4. Messaging runtime `Config.duration` / `Duration` cleanup.
|
4. FlowManager/service `Effect.fn` normalization.
|
||||||
|
|
||||||
## No-Op Rules
|
## No-Op Rules
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
GraphRagResponse,
|
GraphRagResponse,
|
||||||
Term,
|
Term,
|
||||||
TextCompletionRequest,
|
TextCompletionRequest,
|
||||||
|
Triple,
|
||||||
loadProcessorRuntimeConfig,
|
loadProcessorRuntimeConfig,
|
||||||
} from "../index.js";
|
} 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(
|
it.effect(
|
||||||
"preserve gateway response extension fields",
|
"preserve gateway response extension fields",
|
||||||
Effect.fnUntraced(function* () {
|
Effect.fnUntraced(function* () {
|
||||||
|
|
|
||||||
|
|
@ -45,30 +45,28 @@ export type Triple = {
|
||||||
readonly s: Term;
|
readonly s: Term;
|
||||||
readonly p: Term;
|
readonly p: Term;
|
||||||
readonly o: Term;
|
readonly o: Term;
|
||||||
readonly g?: Term;
|
readonly g?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Triple: S.Codec<Triple, Triple> = S.suspend(() =>
|
export const Triple: S.Codec<Triple, Triple> = S.Struct({
|
||||||
S.Struct({
|
s: S.suspend((): S.Codec<Term, Term> => Term),
|
||||||
s: Term,
|
p: S.suspend((): S.Codec<Term, Term> => Term),
|
||||||
p: Term,
|
o: S.suspend((): S.Codec<Term, Term> => Term),
|
||||||
o: Term,
|
g: S.optionalKey(S.String),
|
||||||
g: S.optionalKey(Term),
|
});
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const TripleTerm: S.Codec<TripleTerm, TripleTerm> = S.suspend(() =>
|
export const TripleTerm: S.Codec<TripleTerm, TripleTerm> = S.Struct({
|
||||||
S.Struct({
|
type: S.tag("TRIPLE"),
|
||||||
type: S.tag("TRIPLE"),
|
triple: S.suspend((): S.Codec<Triple, Triple> => Triple),
|
||||||
triple: Triple,
|
});
|
||||||
})
|
|
||||||
);
|
|
||||||
export interface TripleTerm {
|
export interface TripleTerm {
|
||||||
readonly type: "TRIPLE";
|
readonly type: "TRIPLE";
|
||||||
readonly triple: Triple;
|
readonly triple: Triple;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Term: S.Codec<Term, Term> = 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({
|
export const Field = S.Struct({
|
||||||
name: S.String,
|
name: S.String,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,12 @@ import {
|
||||||
dispatcherManagerIsCompleteResponse,
|
dispatcherManagerIsCompleteResponse,
|
||||||
makeDispatcherManager,
|
makeDispatcherManager,
|
||||||
} from "../gateway/dispatch/manager.js";
|
} from "../gateway/dispatch/manager.js";
|
||||||
|
import {
|
||||||
|
clientTermToInternal,
|
||||||
|
internalTermToClient,
|
||||||
|
translateRequestEffect,
|
||||||
|
translateResponseEffect,
|
||||||
|
} from "../gateway/dispatch/serialize.js";
|
||||||
import type {
|
import type {
|
||||||
BackendConsumer,
|
BackendConsumer,
|
||||||
BackendProducer,
|
BackendProducer,
|
||||||
|
|
@ -153,6 +159,69 @@ class DispatchBackend implements PubSubBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("gateway dispatcher manager", () => {
|
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 () => {
|
it("caches Effect requestors as scoped handles", async () => {
|
||||||
const backend = new DispatchBackend();
|
const backend = new DispatchBackend();
|
||||||
const manager = makeDispatcherManager({
|
const manager = makeDispatcherManager({
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import type {
|
||||||
Triple,
|
Triple,
|
||||||
} from "@trustgraph/base";
|
} from "@trustgraph/base";
|
||||||
import {Term as TermSchema} 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 O from "effect/Option";
|
||||||
import * as Predicate from "effect/Predicate";
|
import * as Predicate from "effect/Predicate";
|
||||||
import * as S from "effect/Schema";
|
import * as S from "effect/Schema";
|
||||||
|
|
@ -33,16 +33,15 @@ const decodeTerm = S.decodeUnknownOption(TermSchema);
|
||||||
* Format a Term to a human-readable string.
|
* Format a Term to a human-readable string.
|
||||||
*/
|
*/
|
||||||
function termToString(term: Term): string {
|
function termToString(term: Term): string {
|
||||||
switch (term.type) {
|
return Match.type<Term>().pipe(
|
||||||
case "IRI":
|
Match.discriminatorsExhaustive("type")({
|
||||||
return term.iri;
|
IRI: (iri) => iri.iri,
|
||||||
case "LITERAL":
|
LITERAL: (literal) => literal.value,
|
||||||
return term.value;
|
BLANK: (blank) => `_:${blank.id}`,
|
||||||
case "BLANK":
|
TRIPLE: (triple) =>
|
||||||
return `_:${term.id}`;
|
`(${termToString(triple.triple.s)} ${termToString(triple.triple.p)} ${termToString(triple.triple.o)})`,
|
||||||
case "TRIPLE":
|
}),
|
||||||
return `(${termToString(term.triple.s)} ${termToString(term.triple.p)} ${termToString(term.triple.o)})`;
|
)(term);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,19 @@
|
||||||
* Python reference: trustgraph-base/trustgraph/messaging/translators/primitives.py
|
* Python reference: trustgraph-base/trustgraph/messaging/translators/primitives.py
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { errorMessage, type Term, type Triple } from "@trustgraph/base";
|
import {
|
||||||
import { Effect } from "effect";
|
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";
|
import * as S from "effect/Schema";
|
||||||
|
|
||||||
// ---------- Client wire format type definitions ----------
|
// ---------- Client wire format type definitions ----------
|
||||||
|
|
@ -65,77 +76,102 @@ interface ClientTriple {
|
||||||
g?: string;
|
g?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Client → Internal ----------
|
const ClientIriTermSchema = S.Struct({
|
||||||
|
t: S.tag("i"),
|
||||||
|
i: S.String,
|
||||||
|
});
|
||||||
|
|
||||||
export function clientTermToInternal(wire: ClientTerm): Term {
|
const ClientBlankTermSchema = S.Struct({
|
||||||
switch (wire.t) {
|
t: S.tag("b"),
|
||||||
case "i":
|
d: S.String,
|
||||||
return { type: "IRI", iri: wire.i };
|
});
|
||||||
case "b":
|
|
||||||
return { type: "BLANK", id: wire.d };
|
const ClientLiteralTermSchema = S.Struct({
|
||||||
case "l":
|
t: S.tag("l"),
|
||||||
return {
|
v: S.String,
|
||||||
type: "LITERAL",
|
dt: S.optionalKey(S.String),
|
||||||
value: wire.v,
|
ln: S.optionalKey(S.String),
|
||||||
...(wire.dt !== undefined ? { datatype: wire.dt } : {}),
|
});
|
||||||
...(wire.ln !== undefined ? { language: wire.ln } : {}),
|
|
||||||
};
|
const ClientTripleSchema: S.Codec<ClientTriple, ClientTriple> = S.Struct({
|
||||||
case "t": {
|
s: S.suspend((): S.Codec<ClientTerm, ClientTerm> => ClientTermSchema),
|
||||||
|
p: S.suspend((): S.Codec<ClientTerm, ClientTerm> => ClientTermSchema),
|
||||||
|
o: S.suspend((): S.Codec<ClientTerm, ClientTerm> => 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<ClientTerm>().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) {
|
if (wire.tr === undefined) {
|
||||||
throw DispatchSerializationError.make({
|
throw DispatchSerializationError.make({
|
||||||
operation: "client-term-to-internal",
|
operation: "client-term-to-internal",
|
||||||
message: "Client triple term is missing tr",
|
message: "Client triple term is missing tr",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return {
|
return TripleTerm.make({
|
||||||
type: "TRIPLE",
|
|
||||||
triple: clientTripleToInternal(wire.tr),
|
triple: clientTripleToInternal(wire.tr),
|
||||||
};
|
});
|
||||||
}
|
},
|
||||||
default:
|
}),
|
||||||
// Defensive: pass through unknown term types
|
);
|
||||||
return wire as unknown as Term;
|
|
||||||
}
|
const internalTermToClientMatch = Match.type<Term>().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 {
|
export function clientTripleToInternal(wire: ClientTriple): Triple {
|
||||||
const result: Triple = {
|
return TripleSchema.make({
|
||||||
s: clientTermToInternal(wire.s),
|
s: clientTermToInternal(wire.s),
|
||||||
p: clientTermToInternal(wire.p),
|
p: clientTermToInternal(wire.p),
|
||||||
o: clientTermToInternal(wire.o),
|
o: clientTermToInternal(wire.o),
|
||||||
};
|
...(wire.g !== undefined ? { g: wire.g } : {}),
|
||||||
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<string, unknown>).g = wire.g;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Internal → Client ----------
|
// ---------- Internal → Client ----------
|
||||||
|
|
||||||
export function internalTermToClient(term: Term): ClientTerm {
|
export function internalTermToClient(term: Term): ClientTerm {
|
||||||
switch (term.type) {
|
return internalTermToClientMatch(term);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function internalTripleToClient(triple: Triple): ClientTriple {
|
export function internalTripleToClient(triple: Triple): ClientTriple {
|
||||||
|
|
@ -144,17 +180,8 @@ export function internalTripleToClient(triple: Triple): ClientTriple {
|
||||||
p: internalTermToClient(triple.p),
|
p: internalTermToClient(triple.p),
|
||||||
o: internalTermToClient(triple.o),
|
o: internalTermToClient(triple.o),
|
||||||
};
|
};
|
||||||
const g = (triple as unknown as Record<string, unknown>).g;
|
if (triple.g !== undefined) {
|
||||||
if (g !== undefined && g !== null) {
|
result.g = triple.g;
|
||||||
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<string, unknown>).iri;
|
|
||||||
if (typeof iri === "string") {
|
|
||||||
result.g = iri;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
@ -166,32 +193,6 @@ export function internalTripleToClient(triple: Triple): ClientTriple {
|
||||||
* A client term is detected by the presence of a `t` property
|
* A client term is detected by the presence of a `t` property
|
||||||
* with value "i", "b", "l", or "t".
|
* 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<string, unknown>).t === "string" &&
|
|
||||||
["i", "b", "l", "t"].includes((v as Record<string, unknown>).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<string, unknown>).type === "string" &&
|
|
||||||
["IRI", "BLANK", "LITERAL", "TRIPLE"].includes(
|
|
||||||
(v as Record<string, unknown>).type as string,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deep-translate all client Terms in a request body to internal format.
|
* Deep-translate all client Terms in a request body to internal format.
|
||||||
* Handles nested objects and arrays.
|
* Handles nested objects and arrays.
|
||||||
|
|
@ -204,11 +205,12 @@ function deepClientToInternal(value: unknown): unknown {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === "object") {
|
if (typeof value === "object") {
|
||||||
if (isClientTerm(value)) {
|
const term = decodeClientTerm(value);
|
||||||
return clientTermToInternal(value);
|
if (O.isSome(term)) {
|
||||||
|
return clientTermToInternal(term.value);
|
||||||
}
|
}
|
||||||
const result: Record<string, unknown> = {};
|
const result: Record<string, unknown> = {};
|
||||||
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
for (const [k, v] of Object.entries(value)) {
|
||||||
result[k] = deepClientToInternal(v);
|
result[k] = deepClientToInternal(v);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -229,11 +231,12 @@ function deepInternalToClient(value: unknown): unknown {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === "object") {
|
if (typeof value === "object") {
|
||||||
if (isInternalTerm(value)) {
|
const term = decodeInternalTerm(value);
|
||||||
return internalTermToClient(value);
|
if (O.isSome(term)) {
|
||||||
|
return internalTermToClient(term.value);
|
||||||
}
|
}
|
||||||
const result: Record<string, unknown> = {};
|
const result: Record<string, unknown> = {};
|
||||||
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
for (const [k, v] of Object.entries(value)) {
|
||||||
result[k] = deepInternalToClient(v);
|
result[k] = deepInternalToClient(v);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import { createClient, Graph } from "falkordb";
|
import { createClient, Graph } from "falkordb";
|
||||||
import { errorMessage, type Term, type Triple } from "@trustgraph/base";
|
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 Predicate from "effect/Predicate";
|
||||||
import * as S from "effect/Schema";
|
import * as S from "effect/Schema";
|
||||||
|
|
||||||
|
|
@ -41,16 +41,14 @@ export interface FalkorDBQueryConfig {
|
||||||
|
|
||||||
function termToValue(term: Term | undefined): string | null {
|
function termToValue(term: Term | undefined): string | null {
|
||||||
if (term === undefined) return null;
|
if (term === undefined) return null;
|
||||||
switch (term.type) {
|
return Match.type<Term>().pipe(
|
||||||
case "IRI":
|
Match.discriminatorsExhaustive("type")({
|
||||||
return term.iri;
|
IRI: (iri) => iri.iri,
|
||||||
case "LITERAL":
|
LITERAL: (literal) => literal.value,
|
||||||
return term.value;
|
BLANK: (blank) => blank.id,
|
||||||
case "BLANK":
|
TRIPLE: () => null,
|
||||||
return term.id;
|
}),
|
||||||
default:
|
)(term);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTerm(value: string): Term {
|
function createTerm(value: string): Term {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import type {
|
||||||
TriplesQueryResponse,
|
TriplesQueryResponse,
|
||||||
} from "@trustgraph/base";
|
} from "@trustgraph/base";
|
||||||
import { errorMessage } 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 O from "effect/Option";
|
||||||
import * as S from "effect/Schema";
|
import * as S from "effect/Schema";
|
||||||
|
|
||||||
|
|
@ -461,16 +461,15 @@ function parseScoredEdges(responseText: string): Array<typeof ScoredEdge.Type> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function termToString(term: Term): string {
|
export function termToString(term: Term): string {
|
||||||
switch (term.type) {
|
return Match.type<Term>().pipe(
|
||||||
case "IRI":
|
Match.discriminatorsExhaustive("type")({
|
||||||
return term.iri;
|
IRI: (iri) => iri.iri,
|
||||||
case "LITERAL":
|
LITERAL: (literal) => literal.value,
|
||||||
return term.value;
|
BLANK: (blank) => `_:${blank.id}`,
|
||||||
case "BLANK":
|
TRIPLE: (triple) =>
|
||||||
return `_:${term.id}`;
|
`(${termToString(triple.triple.s)} ${termToString(triple.triple.p)} ${termToString(triple.triple.o)})`,
|
||||||
case "TRIPLE":
|
}),
|
||||||
return `(${termToString(term.triple.s)} ${termToString(term.triple.p)} ${termToString(term.triple.o)})`;
|
)(term);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stringToTerm(value: string): Term {
|
export function stringToTerm(value: string): Term {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { errorMessage, type Term } from "@trustgraph/base";
|
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 O from "effect/Option";
|
||||||
import * as S from "effect/Schema";
|
import * as S from "effect/Schema";
|
||||||
import { makeQdrantClient, type QdrantClientFactory, type QdrantClientLike } from "../../qdrant/client.js";
|
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 {
|
function getTermValue(term: Term): string | null {
|
||||||
switch (term.type) {
|
return Match.type<Term>().pipe(
|
||||||
case "IRI":
|
Match.discriminatorsExhaustive("type")({
|
||||||
return term.iri;
|
IRI: (iri) => iri.iri,
|
||||||
case "LITERAL":
|
LITERAL: (literal) => literal.value,
|
||||||
return term.value;
|
BLANK: (blank) => blank.id,
|
||||||
case "BLANK":
|
TRIPLE: () => null,
|
||||||
return term.id;
|
}),
|
||||||
case "TRIPLE":
|
)(term);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QdrantGraphEmbeddingsStore {
|
export interface QdrantGraphEmbeddingsStore {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
import { createClient, Graph } from "falkordb";
|
import { createClient, Graph } from "falkordb";
|
||||||
import { errorMessage, type Term, type Triple } from "@trustgraph/base";
|
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";
|
import * as S from "effect/Schema";
|
||||||
|
|
||||||
export interface FalkorDBClosableClient {
|
export interface FalkorDBClosableClient {
|
||||||
|
|
@ -40,16 +40,14 @@ export interface FalkorDBConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTermValue(term: Term): string {
|
function getTermValue(term: Term): string {
|
||||||
switch (term.type) {
|
return Match.type<Term>().pipe(
|
||||||
case "IRI":
|
Match.discriminatorsExhaustive("type")({
|
||||||
return term.iri;
|
IRI: (iri) => iri.iri,
|
||||||
case "LITERAL":
|
LITERAL: (literal) => literal.value,
|
||||||
return term.value;
|
BLANK: (blank) => blank.id,
|
||||||
case "BLANK":
|
TRIPLE: (triple) => getTermValue(triple.triple.s),
|
||||||
return term.id;
|
}),
|
||||||
case "TRIPLE":
|
)(term);
|
||||||
return getTermValue(term.triple.s);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FalkorDBTriplesStore {
|
export interface FalkorDBTriplesStore {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { Triple, Term } from "@trustgraph/client";
|
import type { Triple, Term } from "@trustgraph/client";
|
||||||
|
import { Match } from "effect";
|
||||||
import type { NodeObject, LinkObject } from "react-force-graph-2d";
|
import type { NodeObject, LinkObject } from "react-force-graph-2d";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -36,16 +37,14 @@ export interface GraphData {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export function termValue(t: Term): string {
|
export function termValue(t: Term): string {
|
||||||
switch (t.t) {
|
return Match.type<Term>().pipe(
|
||||||
case "i":
|
Match.discriminatorsExhaustive("t")({
|
||||||
return t.i;
|
i: (iri) => iri.i,
|
||||||
case "l":
|
l: (literal) => literal.v,
|
||||||
return t.v;
|
b: (blank) => blank.d,
|
||||||
case "b":
|
t: () => "[triple]",
|
||||||
return t.d;
|
}),
|
||||||
case "t":
|
)(t);
|
||||||
return "[triple]";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isIri(t: Term): boolean {
|
export function isIri(t: Term): boolean {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue