diff --git a/ts/packages/base/src/processor/flow-processor.ts b/ts/packages/base/src/processor/flow-processor.ts index a5c83e7f..15777d02 100644 --- a/ts/packages/base/src/processor/flow-processor.ts +++ b/ts/packages/base/src/processor/flow-processor.ts @@ -18,7 +18,7 @@ import { } from "./async-processor.js"; import type { Spec } from "../spec/types.js"; import type { BackendConsumer, PubSubBackend } from "../backend/types.js"; -import type { FlowDefinition } from "./flow.js"; +import { FlowDefinition } from "./flow.js"; import { Flow, } from "./flow.js"; import { topics } from "../schema/topics.js"; import type { @@ -128,14 +128,9 @@ const ConfigPushSchema = S.Struct({ config: S.Record(S.String, S.Unknown), }); -const FlowDefinitionSchema = S.Struct({ - topics: S.optionalKey(S.Record(S.String, S.String)), - parameters: S.optionalKey(S.Record(S.String, S.Unknown)), -}); +const FlowDefinitions = S.Record(S.String, FlowDefinition); -const FlowDefinitionsSchema = S.Record(S.String, FlowDefinitionSchema); - -const decodeFlowDefinitions = S.decodeUnknownOption(FlowDefinitionsSchema); +const decodeFlowDefinitions = S.decodeUnknownOption(FlowDefinitions); export function runFlowProcessorDefinitionScoped< FlowRequirements = never, diff --git a/ts/packages/base/src/processor/flow.ts b/ts/packages/base/src/processor/flow.ts index ceaa9e36..3d8ea3d9 100644 --- a/ts/packages/base/src/processor/flow.ts +++ b/ts/packages/base/src/processor/flow.ts @@ -43,12 +43,14 @@ import type { ProducerSpec } from "../spec/producer-spec.js"; import type { RequestResponseSpec } from "../spec/request-response-spec.js"; import type { Spec, SpecRuntimeRequirements } from "../spec/types.js"; -export interface FlowDefinition { +export class FlowDefinition extends S.Class("FlowDefinition")({ /** Topic overrides keyed by spec name */ - topics?: Record; + topics: S.optionalKey(S.Record(S.String, S.String)), /** Parameter values keyed by spec name */ - parameters?: Record; -} + parameters: S.optionalKey(S.Record(S.String, S.Unknown)), +}, { + description: "Per-flow configuration: topic overrides and parameter values keyed by spec name.", +}) {} export interface FlowProducer { readonly send: (id: string, message: T) => Effect.Effect; diff --git a/ts/packages/base/src/runtime/messaging-config.ts b/ts/packages/base/src/runtime/messaging-config.ts index b5ef1bc8..67d1f892 100644 --- a/ts/packages/base/src/runtime/messaging-config.ts +++ b/ts/packages/base/src/runtime/messaging-config.ts @@ -3,22 +3,25 @@ */ import { Config, Duration, Effect } from "effect"; +import * as S from "effect/Schema"; -export interface MessagingRuntimeConfig { - readonly consumerReceiveTimeout: Duration.Duration; - readonly consumerErrorBackoff: Duration.Duration; - readonly rateLimitRetry: Duration.Duration; - readonly rateLimitTimeout: Duration.Duration; - readonly requestTimeout: Duration.Duration; -} +export class MessagingRuntimeConfig extends S.Class("MessagingRuntimeConfig")({ + consumerReceiveTimeout: S.Duration, + consumerErrorBackoff: S.Duration, + rateLimitRetry: S.Duration, + rateLimitTimeout: S.Duration, + requestTimeout: S.Duration, +}, { + description: "Messaging runtime timing windows for consumer receive, backoff, rate-limit retry, and request timeout.", +}) {} -export const defaultMessagingRuntimeConfig: MessagingRuntimeConfig = { +export const defaultMessagingRuntimeConfig: MessagingRuntimeConfig = MessagingRuntimeConfig.make({ consumerReceiveTimeout: Duration.millis(2_000), consumerErrorBackoff: Duration.millis(1_000), rateLimitRetry: Duration.millis(10_000), rateLimitTimeout: Duration.millis(7_200_000), requestTimeout: Duration.millis(300_000), -}; +}); const durationConfig = (name: string, defaultValue: Duration.Duration) => Config.duration(name).pipe( @@ -48,11 +51,11 @@ export const loadMessagingRuntimeConfig = Effect.fn("loadMessagingRuntimeConfig" defaultMessagingRuntimeConfig.requestTimeout, ); - return { + return MessagingRuntimeConfig.make({ consumerReceiveTimeout, consumerErrorBackoff, rateLimitRetry, rateLimitTimeout, requestTimeout, - } satisfies MessagingRuntimeConfig; + }); }); diff --git a/ts/packages/cli/src/commands/util.ts b/ts/packages/cli/src/commands/util.ts index f21243cc..f0a1deb3 100644 --- a/ts/packages/cli/src/commands/util.ts +++ b/ts/packages/cli/src/commands/util.ts @@ -16,12 +16,12 @@ import * as S from "effect/Schema"; import * as Command from "effect/unstable/cli/Command"; import * as Flag from "effect/unstable/cli/Flag"; -export interface CliOpts { - gateway: string; - user: string; - token?: string; - flow: string; -} +export class CliOpts extends S.Class("CliOpts")({ + gateway: S.String, + user: S.String, + token: S.optionalKey(S.String), + flow: S.String, +}, { description: "Resolved TrustGraph CLI connection options." }) {} export const rootCommand = Command.make("tg").pipe( Command.withDescription("TrustGraph CLI - interact with TrustGraph services"), diff --git a/ts/packages/flow/src/agent/react/service.ts b/ts/packages/flow/src/agent/react/service.ts index 1d22e5fb..303fa60b 100644 --- a/ts/packages/flow/src/agent/react/service.ts +++ b/ts/packages/flow/src/agent/react/service.ts @@ -430,7 +430,7 @@ const onAgentRequest = Effect.fn("AgentService.onRequest")(function* ( chunk_type: "explain", content: "", explain_id: explain.explainId, - explain_triples: explain.triples, + explain_triples: [...explain.triples], }); } diff --git a/ts/packages/flow/src/agent/react/tools.ts b/ts/packages/flow/src/agent/react/tools.ts index 91290387..1c6fed65 100644 --- a/ts/packages/flow/src/agent/react/tools.ts +++ b/ts/packages/flow/src/agent/react/tools.ts @@ -15,10 +15,8 @@ import type { TriplesQueryResponse, ToolRequest, ToolResponse, - Term, - Triple, } from "@trustgraph/base"; -import {Term as TermSchema} from "@trustgraph/base"; +import { Term, Triple } from "@trustgraph/base"; import { Effect, Match } from "effect"; import * as O from "effect/Option"; import * as Predicate from "effect/Predicate"; @@ -27,7 +25,7 @@ import type { AgentTool, ToolArg } from "./types.js"; import { agentToolError, } from "./types.js"; const decodeJsonUnknown = S.decodeUnknownOption(S.UnknownFromJsonString); -const decodeTerm = S.decodeUnknownOption(TermSchema); +const decodeTerm = S.decodeUnknownOption(Term); /** * Format a Term to a human-readable string. @@ -64,10 +62,10 @@ function parseQuestion(input: string): string { /** * Explain data extracted from a graph-rag response. */ -export interface ExplainData { - explainId: string; - triples: Triple[]; -} +export class ExplainData extends S.Class("ExplainData")({ + explainId: S.String, + triples: S.Array(Triple), +}, { description: "Explain payload extracted from a graph-rag response: id plus supporting triples." }) {} /** * Query the knowledge graph for information about entities and their relationships. diff --git a/ts/packages/flow/src/agent/react/types.ts b/ts/packages/flow/src/agent/react/types.ts index 23daefbc..2fd26a62 100644 --- a/ts/packages/flow/src/agent/react/types.ts +++ b/ts/packages/flow/src/agent/react/types.ts @@ -20,11 +20,11 @@ export const agentToolError = (operation: string, cause: unknown): AgentToolErro message: errorMessage(cause), }); -export interface ToolArg { - name: string; - type: string; - description: string; -} +export class ToolArg extends S.Class("ToolArg")({ + name: S.String, + type: S.String, + description: S.String, +}, { description: "A named, typed argument accepted by an agent tool." }) {} export interface AgentTool { name: string; @@ -43,10 +43,10 @@ export type ReActState = | "final_answer" | "complete"; -export interface ParsedEvent { - type: "thought" | "action" | "action_input" | "final_answer"; - content: string; -} +export class ParsedEvent extends S.Class("ParsedEvent")({ + type: S.Literals(["thought", "action", "action_input", "final_answer"]), + content: S.String, +}, { description: "A parsed ReAct stream event with its section content." }) {} export type OnThought = (text: string, isFinal: boolean) => Effect.Effect; export type OnObservation = (text: string, isFinal: boolean) => Effect.Effect; diff --git a/ts/packages/flow/src/librarian/collection-manager.ts b/ts/packages/flow/src/librarian/collection-manager.ts index a00d82c5..e21d1362 100644 --- a/ts/packages/flow/src/librarian/collection-manager.ts +++ b/ts/packages/flow/src/librarian/collection-manager.ts @@ -8,14 +8,15 @@ import * as MutableHashMap from "effect/MutableHashMap"; import * as O from "effect/Option"; +import * as S from "effect/Schema"; -export interface CollectionEntry { - user: string; - collection: string; - name: string; - description: string; - tags: string[]; -} +export class CollectionEntry extends S.Class("CollectionEntry")({ + user: S.String, + collection: S.String, + name: S.String, + description: S.String, + tags: S.Array(S.String), +}, { description: "A librarian collection registration with its metadata tags." }) {} export interface CollectionManager { readonly listCollections: (user: string) => CollectionEntry[]; diff --git a/ts/packages/flow/src/model/text-completion/common.ts b/ts/packages/flow/src/model/text-completion/common.ts index 1ae39541..0bc23527 100644 --- a/ts/packages/flow/src/model/text-completion/common.ts +++ b/ts/packages/flow/src/model/text-completion/common.ts @@ -39,10 +39,10 @@ export type TextCompletionRuntimeError = | TextCompletionProviderError | TooManyRequestsError; -export interface LanguageModelProviderRequest { - readonly model: string; - readonly temperature: number; -} +export class LanguageModelProviderRequest extends S.Class("LanguageModelProviderRequest")({ + model: S.String, + temperature: S.Finite, +}, { description: "Resolved model id and temperature for a language-model call." }) {} export interface LanguageModelProviderOptions { readonly provider: string; diff --git a/ts/packages/flow/src/prompt/template.ts b/ts/packages/flow/src/prompt/template.ts index 063083dd..e439a2b5 100644 --- a/ts/packages/flow/src/prompt/template.ts +++ b/ts/packages/flow/src/prompt/template.ts @@ -47,10 +47,10 @@ import * as MutableHashMap from "effect/MutableHashMap"; import * as O from "effect/Option"; import * as S from "effect/Schema"; -export interface PromptTemplate { - system: string; - prompt: string; -} +export class PromptTemplate extends S.Class("PromptTemplate")({ + system: S.String, + prompt: S.String, +}, { description: "A prompt template: system preamble plus prompt body." }) {} export interface PromptTemplateConfig extends ProcessorConfig { configKey?: string; diff --git a/ts/packages/flow/src/qdrant/client.ts b/ts/packages/flow/src/qdrant/client.ts index e418bf34..1835ea49 100644 --- a/ts/packages/flow/src/qdrant/client.ts +++ b/ts/packages/flow/src/qdrant/client.ts @@ -4,22 +4,22 @@ import { errorMessage } from "@trustgraph/base"; import { Effect } from "effect"; import * as S from "effect/Schema"; -export interface QdrantCollectionStatus { - readonly exists: boolean; -} +export class QdrantCollectionStatus extends S.Class("QdrantCollectionStatus")({ + exists: S.Boolean, +}, { description: "Qdrant collection existence probe result." }) {} -export interface QdrantCollectionDescription { - readonly name: string; -} +export class QdrantCollectionDescription extends S.Class("QdrantCollectionDescription")({ + name: S.String, +}, { description: "A named Qdrant collection." }) {} -export interface QdrantCollections { - readonly collections: ReadonlyArray; -} +export class QdrantCollections extends S.Class("QdrantCollections")({ + collections: S.Array(QdrantCollectionDescription), +}, { description: "Qdrant collection listing." }) {} -export interface QdrantScoredPoint { - readonly score: number; - readonly payload?: unknown; -} +export class QdrantScoredPoint extends S.Class("QdrantScoredPoint")({ + score: S.Finite, + payload: S.optionalKey(S.Unknown), +}, { description: "A scored Qdrant search hit with optional payload." }) {} export class QdrantClientError extends S.TaggedErrorClass()("QdrantClientError", { message: S.String, diff --git a/ts/packages/flow/src/query/embeddings/qdrant-doc.ts b/ts/packages/flow/src/query/embeddings/qdrant-doc.ts index b988fbb4..a86052ec 100644 --- a/ts/packages/flow/src/query/embeddings/qdrant-doc.ts +++ b/ts/packages/flow/src/query/embeddings/qdrant-doc.ts @@ -20,18 +20,18 @@ export interface QdrantDocQueryConfig { clientFactory?: QdrantClientFactory; } -export interface ChunkMatch { - chunkId: string; - score: number; - content?: string; -} +export class ChunkMatch extends S.Class("ChunkMatch")({ + chunkId: S.String, + score: S.Finite, + content: S.optionalKey(S.String), +}, { description: "A scored document-chunk match from embeddings query." }) {} -export interface DocEmbeddingsQueryRequest { - vector: number[]; - user: string; - collection: string; - limit: number; -} +export class DocEmbeddingsQueryRequest extends S.Class("DocEmbeddingsQueryRequest")({ + vector: S.Array(S.Finite), + user: S.String, + collection: S.String, + limit: S.Finite, +}, { description: "Document embeddings similarity query request." }) {} export class QdrantDocEmbeddingsQueryError extends S.TaggedErrorClass()( "QdrantDocEmbeddingsQueryError", diff --git a/ts/packages/flow/src/query/embeddings/qdrant-graph.ts b/ts/packages/flow/src/query/embeddings/qdrant-graph.ts index 1e70691c..c94b1e14 100644 --- a/ts/packages/flow/src/query/embeddings/qdrant-graph.ts +++ b/ts/packages/flow/src/query/embeddings/qdrant-graph.ts @@ -10,8 +10,7 @@ * Python reference: trustgraph-flow/trustgraph/query/graph_embeddings/qdrant/service.py */ -import type { Term } from "@trustgraph/base"; -import { errorMessage, } from "@trustgraph/base"; +import { Term, errorMessage } from "@trustgraph/base"; import { Config, Context, Effect, Layer } from "effect"; import * as O from "effect/Option"; import * as S from "effect/Schema"; @@ -24,17 +23,17 @@ export interface QdrantGraphQueryConfig { clientFactory?: QdrantClientFactory; } -export interface EntityMatch { - entity: Term; - score: number; -} +export class EntityMatch extends S.Class("EntityMatch")({ + entity: Term, + score: S.Finite, +}, { description: "A scored graph-entity match from embeddings query." }) {} -export interface GraphEmbeddingsQueryRequest { - vector: number[]; - user: string; - collection: string; - limit: number; -} +export class GraphEmbeddingsQueryRequest extends S.Class("GraphEmbeddingsQueryRequest")({ + vector: S.Array(S.Finite), + user: S.String, + collection: S.String, + limit: S.Finite, +}, { description: "Graph embeddings similarity query request." }) {} export class QdrantGraphEmbeddingsQueryError extends S.TaggedErrorClass()( "QdrantGraphEmbeddingsQueryError", diff --git a/ts/packages/flow/src/retrieval/graph-rag-service.ts b/ts/packages/flow/src/retrieval/graph-rag-service.ts index e0e7230e..80ab1772 100644 --- a/ts/packages/flow/src/retrieval/graph-rag-service.ts +++ b/ts/packages/flow/src/retrieval/graph-rag-service.ts @@ -138,7 +138,7 @@ const onGraphRagRequest = Effect.fn("GraphRagService.onRequest")(function* ( endOfStream: true, message_type: "explain", explain_id: `explain-${requestId}`, - explain_triples: result.subgraph, + explain_triples: [...result.subgraph], }; yield* producer.send(requestId, response); diff --git a/ts/packages/flow/src/retrieval/graph-rag.ts b/ts/packages/flow/src/retrieval/graph-rag.ts index 8471d952..823c9e1e 100644 --- a/ts/packages/flow/src/retrieval/graph-rag.ts +++ b/ts/packages/flow/src/retrieval/graph-rag.ts @@ -16,23 +16,22 @@ import type { Term, TextCompletionRequest, TextCompletionResponse, - Triple, TriplesQueryRequest, TriplesQueryResponse, } from "@trustgraph/base"; -import { errorMessage } from "@trustgraph/base"; +import { Triple, errorMessage } from "@trustgraph/base"; import { Context, Effect, Layer, Match } from "effect"; import * as O from "effect/Option"; import * as S from "effect/Schema"; -export interface GraphRagConfig { - entityLimit?: number; - tripleLimit?: number; - maxSubgraphSize?: number; - maxPathLength?: number; - edgeScoreLimit?: number; - edgeLimit?: number; -} +export class GraphRagConfig extends S.Class("GraphRagConfig")({ + entityLimit: S.optionalKey(S.Finite), + tripleLimit: S.optionalKey(S.Finite), + maxSubgraphSize: S.optionalKey(S.Finite), + maxPathLength: S.optionalKey(S.Finite), + edgeScoreLimit: S.optionalKey(S.Finite), + edgeLimit: S.optionalKey(S.Finite), +}, { description: "Graph RAG retrieval tuning limits." }) {} export interface GraphRagClients { llm: EffectRequestResponse; @@ -53,10 +52,10 @@ export interface GraphRagQueryOptions { readonly chunkCallback?: ChunkCallback; } -export interface GraphRagResult { - answer: string; - subgraph: Triple[]; -} +export class GraphRagResult extends S.Class("GraphRagResult")({ + answer: S.String, + subgraph: S.Array(Triple), +}, { description: "Graph RAG answer with the supporting subgraph." }) {} interface NormalizedGraphRagConfig { entityLimit: number; diff --git a/ts/packages/flow/src/storage/embeddings/qdrant-doc.ts b/ts/packages/flow/src/storage/embeddings/qdrant-doc.ts index 637f72d1..7e2004eb 100644 --- a/ts/packages/flow/src/storage/embeddings/qdrant-doc.ts +++ b/ts/packages/flow/src/storage/embeddings/qdrant-doc.ts @@ -22,17 +22,17 @@ export interface QdrantDocEmbeddingsConfig { clientFactory?: QdrantClientFactory; } -export interface DocEmbeddingChunk { - chunkId: string; - vector: number[]; - content?: string; -} +export class DocEmbeddingChunk extends S.Class("DocEmbeddingChunk")({ + chunkId: S.String, + vector: S.Array(S.Finite), + content: S.optionalKey(S.String), +}, { description: "A document chunk paired with its embedding vector." }) {} -export interface DocEmbeddingsMessage { - user: string; - collection: string; - chunks: DocEmbeddingChunk[]; -} +export class DocEmbeddingsMessage extends S.Class("DocEmbeddingsMessage")({ + user: S.String, + collection: S.String, + chunks: S.Array(DocEmbeddingChunk), +}, { description: "Document embeddings store message: chunks to upsert for a user collection." }) {} export class QdrantDocEmbeddingsStoreError extends S.TaggedErrorClass()( "QdrantDocEmbeddingsStoreError", diff --git a/ts/packages/flow/src/storage/embeddings/qdrant-graph.ts b/ts/packages/flow/src/storage/embeddings/qdrant-graph.ts index 23e2ca9a..aef79a04 100644 --- a/ts/packages/flow/src/storage/embeddings/qdrant-graph.ts +++ b/ts/packages/flow/src/storage/embeddings/qdrant-graph.ts @@ -8,8 +8,7 @@ * Python reference: trustgraph-flow/trustgraph/storage/graph_embeddings/qdrant/write.py */ -import type { Term } from "@trustgraph/base"; -import { errorMessage, } from "@trustgraph/base"; +import { Term, errorMessage } from "@trustgraph/base"; import { Config, Context, Effect, Layer, Match, Random } from "effect"; import * as MutableHashSet from "effect/MutableHashSet"; import * as O from "effect/Option"; @@ -23,17 +22,17 @@ export interface QdrantGraphEmbeddingsConfig { clientFactory?: QdrantClientFactory; } -export interface GraphEmbeddingEntity { - entity: Term; - vector: number[]; - chunkId?: string; -} +export class GraphEmbeddingEntity extends S.Class("GraphEmbeddingEntity")({ + entity: Term, + vector: S.Array(S.Finite), + chunkId: S.optionalKey(S.String), +}, { description: "A graph entity paired with its embedding vector." }) {} -export interface GraphEmbeddingsMessage { - user: string; - collection: string; - entities: GraphEmbeddingEntity[]; -} +export class GraphEmbeddingsMessage extends S.Class("GraphEmbeddingsMessage")({ + user: S.String, + collection: S.String, + entities: S.Array(GraphEmbeddingEntity), +}, { description: "Graph embeddings store message: entities to upsert for a user collection." }) {} export class QdrantGraphEmbeddingsStoreError extends S.TaggedErrorClass()( "QdrantGraphEmbeddingsStoreError", diff --git a/ts/packages/mcp/src/server-effect.ts b/ts/packages/mcp/src/server-effect.ts index a45eb53d..47f3043b 100644 --- a/ts/packages/mcp/src/server-effect.ts +++ b/ts/packages/mcp/src/server-effect.ts @@ -1217,16 +1217,18 @@ export interface TrustGraphMcpOptions { readonly port?: number | undefined } -export interface TrustGraphMcpConfigShape { - readonly gatewayUrl: string - readonly user: string - readonly token: string | undefined - readonly flowId: string - readonly name: string - readonly version: string - readonly mcpPath: HttpRouter.PathInput - readonly port: number -} +const McpPathInput = S.Union([S.Literal("*"), S.TemplateLiteral(["/", S.String])]) + +export class TrustGraphMcpConfigShape extends S.Class("TrustGraphMcpConfigShape")({ + gatewayUrl: S.String, + user: S.String, + token: S.UndefinedOr(S.String), + flowId: S.String, + name: S.String, + version: S.String, + mcpPath: McpPathInput, + port: S.Finite, +}, { description: "Resolved TrustGraph MCP server configuration." }) {} const readNonEmpty = (value: string | undefined): string | undefined => value !== undefined && value.length > 0 ? value : undefined diff --git a/ts/packages/workbench/src/atoms/workbench.ts b/ts/packages/workbench/src/atoms/workbench.ts index fb40752c..b5f58a7d 100644 --- a/ts/packages/workbench/src/atoms/workbench.ts +++ b/ts/packages/workbench/src/atoms/workbench.ts @@ -71,6 +71,44 @@ type WorkbenchError = WorkbenchPromiseError; const isWorkbenchPromiseError = S.is(WorkbenchPromiseError); +const ClientTriple: S.Codec = S.suspend(() => + S.Struct({ + s: ClientTerm, + p: ClientTerm, + o: ClientTerm, + g: S.optionalKey(S.String), + }) +); + +const ClientTerm: S.Codec = S.suspend(() => + S.Union([ + S.Struct({ + t: S.Literal("i"), + i: S.String, + }), + S.Struct({ + t: S.Literal("b"), + d: S.String, + }), + S.Struct({ + t: S.Literal("l"), + v: S.String, + dt: S.optionalKey(S.String), + ln: S.optionalKey(S.String), + }), + S.Struct({ + t: S.Literal("t"), + tr: S.optionalKey(ClientTriple), + }), + ]) +); + +const ClientExplainEvent: S.Codec = S.Struct({ + explainId: S.String, + explainGraph: S.String, + explainTriples: S.optionalKey(S.Array(ClientTriple).pipe(S.mutable)), +}); + function errorMessage(error: unknown): string { if (isWorkbenchPromiseError(error)) return error.message; if (Predicate.isObject(error) && Predicate.hasProperty(error, "message")) { @@ -146,25 +184,25 @@ const mutationCounter = Metric.counter("trustgraph_workbench_mutation_total", { // Shared types // --------------------------------------------------------------------------- -export interface FeatureSwitches { - flowClasses: boolean; - submissions: boolean; - tokenCost: boolean; - schemas: boolean; - structuredQuery: boolean; - ontologyEditor: boolean; - agentTools: boolean; - mcpTools: boolean; - llmModels: boolean; -} +export class FeatureSwitches extends S.Class("FeatureSwitches")({ + flowClasses: S.Boolean, + submissions: S.Boolean, + tokenCost: S.Boolean, + schemas: S.Boolean, + structuredQuery: S.Boolean, + ontologyEditor: S.Boolean, + agentTools: S.Boolean, + mcpTools: S.Boolean, + llmModels: S.Boolean, +}, { description: "Workbench feature visibility switches." }) {} -export interface Settings { - user: string; - apiKey: string; - collection: string; - gatewayUrl: string; - featureSwitches: FeatureSwitches; -} +export class Settings extends S.Class("Settings")({ + user: S.String, + apiKey: S.String, + collection: S.String, + gatewayUrl: S.String, + featureSwitches: FeatureSwitches, +}, { description: "Persisted workbench connection and display settings." }) {} export interface WorkbenchApiFactory { readonly create: (settings: Settings) => BaseApi; @@ -176,31 +214,31 @@ export type ChatMode = "graph-rag" | "document-rag" | "agent"; export type MessageRole = "user" | "assistant" | "system"; export type AgentPhase = "think" | "observe" | "answer"; -export interface ChatMessage { - id: string; - role: MessageRole; - content: string; - timestamp: number; - isStreaming?: boolean; - metadata?: { - model?: string; - inTokens?: number; - outTokens?: number; - }; - agentPhases?: { - think: string; - observe: string; - answer: string; - }; - activePhase?: AgentPhase; - explainEvents?: ExplainEvent[]; -} +export class ChatMessage extends S.Class("ChatMessage")({ + id: S.String, + role: S.Literals(["user", "assistant", "system"]), + content: S.String, + timestamp: S.Finite, + isStreaming: S.optionalKey(S.Boolean), + metadata: S.optionalKey(S.Struct({ + model: S.optionalKey(S.String), + inTokens: S.optionalKey(S.Finite), + outTokens: S.optionalKey(S.Finite), + })), + agentPhases: S.optionalKey(S.Struct({ + think: S.String, + observe: S.String, + answer: S.String, + })), + activePhase: S.optionalKey(S.Literals(["think", "observe", "answer"])), + explainEvents: S.optionalKey(S.Array(ClientExplainEvent).pipe(S.mutable)), +}, { description: "A rendered chat transcript message." }) {} -export interface ConversationState { - messages: ChatMessage[]; - input: string; - chatMode: ChatMode; -} +export class ConversationState extends S.Class("ConversationState")({ + messages: S.Array(ChatMessage).pipe(S.mutable), + input: S.String, + chatMode: S.Literals(["graph-rag", "document-rag", "agent"]), +}, { description: "Persisted workbench chat state." }) {} export interface FlowSummary { id: string; @@ -216,60 +254,60 @@ export interface ProcessingMetadata { [key: string]: unknown; } -export interface UploadProgress { - phase: "preparing" | "uploading" | "finalizing"; - chunksTotal: number; - chunksUploaded: number; - bytesTotal: number; - bytesUploaded: number; -} +export class UploadProgress extends S.Class("UploadProgress")({ + phase: S.Literals(["preparing", "uploading", "finalizing"]), + chunksTotal: S.Finite, + chunksUploaded: S.Finite, + bytesTotal: S.Finite, + bytesUploaded: S.Finite, +}, { description: "Current chunked document upload progress." }) {} -export interface UploadForm { - file: File | null; - title: string; - tags: string; - comments: string; - uploading: boolean; - dragOver: boolean; - progress: UploadProgress | null; -} +export class UploadForm extends S.Class("UploadForm")({ + file: S.NullOr(S.File), + title: S.String, + tags: S.String, + comments: S.String, + uploading: S.Boolean, + dragOver: S.Boolean, + progress: S.NullOr(UploadProgress), +}, { description: "Workbench document upload form state." }) {} -export interface McpServerConfig { - url: string; - "remote-name"?: string; - "auth-token"?: string; -} +export class McpServerConfig extends S.Class("McpServerConfig")({ + url: S.String, + "remote-name": S.optionalKey(S.String), + "auth-token": S.optionalKey(S.String), +}, { description: "Workbench MCP server config entry payload." }) {} -export interface McpServerEntry { - key: string; - config: McpServerConfig; -} +export class McpServerEntry extends S.Class("McpServerEntry")({ + key: S.String, + config: McpServerConfig, +}, { description: "Workbench MCP server config entry." }) {} -export interface ToolArgument { - name: string; - type: string; - description: string; -} +export class ToolArgument extends S.Class("ToolArgument")({ + name: S.String, + type: S.String, + description: S.String, +}, { description: "Workbench MCP tool argument descriptor." }) {} -export interface ToolConfig { - type: string; - name: string; - description: string; - "mcp-tool"?: string; - group?: string[]; - arguments?: ToolArgument[]; -} +export class ToolConfig extends S.Class("ToolConfig")({ + type: S.String, + name: S.String, + description: S.String, + "mcp-tool": S.optionalKey(S.String), + group: S.optionalKey(S.Array(S.String).pipe(S.mutable)), + arguments: S.optionalKey(S.Array(ToolArgument).pipe(S.mutable)), +}, { description: "Workbench tool config entry payload." }) {} -export interface ToolEntry { - key: string; - config: ToolConfig; -} +export class ToolEntry extends S.Class("ToolEntry")({ + key: S.String, + config: ToolConfig, +}, { description: "Workbench tool config entry." }) {} -export interface TokenCost { - model: string; - input_price: number; - output_price: number; -} +export class TokenCost extends S.Class("TokenCost")({ + model: S.String, + input_price: S.Finite, + output_price: S.Finite, +}, { description: "Model token pricing row." }) {} export interface CollectionSummary { id?: string; @@ -280,61 +318,61 @@ export interface CollectionSummary { [key: string]: unknown; } -export interface Notification { - id: string; - type: "success" | "error" | "warning" | "info"; - title: string; - description?: string; -} +export class Notification extends S.Class("Notification")({ + id: S.String, + type: S.Literals(["success", "error", "warning", "info"]), + title: S.String, + description: S.optionalKey(S.String), +}, { description: "Transient workbench notification toast." }) {} -export interface McpServerForm { - key: string; - url: string; - remoteName: string; - authToken: string; - showToken: boolean; - saving: boolean; - keyError: string; -} +export class McpServerForm extends S.Class("McpServerForm")({ + key: S.String, + url: S.String, + remoteName: S.String, + authToken: S.String, + showToken: S.Boolean, + saving: S.Boolean, + keyError: S.String, +}, { description: "Editable MCP server dialog state." }) {} -export interface McpToolForm { - key: string; - name: string; - description: string; - mcpTool: string; - group: string; - args: ToolArgument[]; - saving: boolean; - keyError: string; -} +export class McpToolForm extends S.Class("McpToolForm")({ + key: S.String, + name: S.String, + description: S.String, + mcpTool: S.String, + group: S.String, + args: S.Array(ToolArgument).pipe(S.mutable), + saving: S.Boolean, + keyError: S.String, +}, { description: "Editable MCP tool dialog state." }) {} -export interface StartFlowForm { - id: string; - blueprint: string; - description: string; - paramsJson: string; - submitting: boolean; - paramsError: string | null; - submitted: boolean; - definitionExpanded: boolean; -} +export class StartFlowForm extends S.Class("StartFlowForm")({ + id: S.String, + blueprint: S.String, + description: S.String, + paramsJson: S.String, + submitting: S.Boolean, + paramsError: S.NullOr(S.String), + submitted: S.Boolean, + definitionExpanded: S.Boolean, +}, { description: "Start-flow dialog form state." }) {} -export interface CollectionForm { - id: string; - name: string; - description: string; - tags: string; - submitting: boolean; -} +export class CollectionForm extends S.Class("CollectionForm")({ + id: S.String, + name: S.String, + description: S.String, + tags: S.String, + submitting: S.Boolean, +}, { description: "Collection creation form state." }) {} -export interface GraphViewState { - searchTerm: string; - selectedNodeId: string | null; - selectedNodeLabel: string | null; - showLabels: boolean; - showTypes: boolean; - nodeLimit: number; -} +export class GraphViewState extends S.Class("GraphViewState")({ + searchTerm: S.String, + selectedNodeId: S.NullOr(S.String), + selectedNodeLabel: S.NullOr(S.String), + showLabels: S.Boolean, + showTypes: S.Boolean, + nodeLimit: S.Finite, +}, { description: "Workbench graph display controls." }) {} const DEFAULT_FEATURE_SWITCHES: FeatureSwitches = { flowClasses: false, @@ -356,50 +394,6 @@ export const DEFAULT_SETTINGS: Settings = { featureSwitches: DEFAULT_FEATURE_SWITCHES, }; -const SettingsSchema = S.Struct({ - user: S.String, - apiKey: S.String, - collection: S.String, - gatewayUrl: S.String, - featureSwitches: S.Struct({ - flowClasses: S.Boolean, - submissions: S.Boolean, - tokenCost: S.Boolean, - schemas: S.Boolean, - structuredQuery: S.Boolean, - ontologyEditor: S.Boolean, - agentTools: S.Boolean, - mcpTools: S.Boolean, - llmModels: S.Boolean, - }), -}); - -const ChatMessageSchema = S.Struct({ - id: S.String, - role: S.Union([S.Literal("user"), S.Literal("assistant"), S.Literal("system")]), - content: S.String, - timestamp: S.Finite, - isStreaming: S.optionalKey(S.Boolean), - metadata: S.optionalKey(S.Struct({ - model: S.optionalKey(S.String), - inTokens: S.optionalKey(S.Finite), - outTokens: S.optionalKey(S.Finite), - })), - agentPhases: S.optionalKey(S.Struct({ - think: S.String, - observe: S.String, - answer: S.String, - })), - activePhase: S.optionalKey(S.Union([S.Literal("think"), S.Literal("observe"), S.Literal("answer")])), - explainEvents: S.optionalKey(S.Array(S.Unknown)), -}); - -const ConversationSchema = S.Struct({ - messages: S.Array(ChatMessageSchema), - input: S.String, - chatMode: S.Union([S.Literal("graph-rag"), S.Literal("document-rag"), S.Literal("agent")]), -}); - const ThemeSchema = S.Union([S.Literal("dark"), S.Literal("light")]); const FlowIdSchema = S.String; @@ -523,11 +517,14 @@ const randomId = Effect.fn("trustgraph.workbench.randomId")(function*(prefix: st function metadataFrom(metadata: StreamingMetadata | undefined): ChatMessage["metadata"] | undefined { if (metadata === undefined) return undefined; - const result: NonNullable = {}; - if (metadata.model !== undefined) result.model = metadata.model; - if (metadata.in_token !== undefined) result.inTokens = metadata.in_token; - if (metadata.out_token !== undefined) result.outTokens = metadata.out_token; - return Object.keys(result).length > 0 ? result : undefined; + if (metadata.model === undefined && metadata.in_token === undefined && metadata.out_token === undefined) { + return undefined; + } + return { + ...(metadata.model !== undefined ? { model: metadata.model } : {}), + ...(metadata.in_token !== undefined ? { inTokens: metadata.in_token } : {}), + ...(metadata.out_token !== undefined ? { outTokens: metadata.out_token } : {}), + }; } function withoutActivePhase(message: ChatMessage): ChatMessage { @@ -609,40 +606,8 @@ const StreamingEnvelopeSchema = S.Struct({ }); type StreamingEnvelope = typeof StreamingEnvelopeSchema.Type; -const ClientTripleSchema: S.Codec = S.suspend(() => - S.Struct({ - s: ClientTermSchema, - p: ClientTermSchema, - o: ClientTermSchema, - g: S.optionalKey(S.String), - }) -); - -const ClientTermSchema: S.Codec = S.suspend(() => - S.Union([ - S.Struct({ - t: S.Literal("i"), - i: S.String, - }), - S.Struct({ - t: S.Literal("b"), - d: S.String, - }), - S.Struct({ - t: S.Literal("l"), - v: S.String, - dt: S.optionalKey(S.String), - ln: S.optionalKey(S.String), - }), - S.Struct({ - t: S.Literal("t"), - tr: S.optionalKey(ClientTripleSchema), - }), - ]) -); - const decodeStreamingEnvelope = S.decodeUnknownOption(StreamingEnvelopeSchema); -const decodeClientTriples = S.decodeUnknownOption(S.Array(ClientTripleSchema).pipe(S.mutable)); +const decodeClientTriples = S.decodeUnknownOption(S.Array(ClientTriple).pipe(S.mutable)); function gatewayHttpBaseUrl(settings: Settings): string { const raw = settings.gatewayUrl.trim(); @@ -1532,7 +1497,7 @@ function withActivity( export const settingsAtom = Atom.kvs({ runtime: workbenchRuntime, key: "trustgraph-workbench-settings-v1", - schema: S.toCodecJson(SettingsSchema), + schema: S.toCodecJson(Settings), defaultValue: legacySettings, }).pipe(Atom.keepAlive) as Atom.Writable; @@ -1553,7 +1518,7 @@ export const flowIdAtom = Atom.kvs({ export const conversationAtom = Atom.kvs({ runtime: workbenchRuntime, key: "trustgraph-workbench-conversation-v1", - schema: S.toCodecJson(ConversationSchema), + schema: S.toCodecJson(ConversationState), defaultValue: legacyConversation, }).pipe(Atom.keepAlive) as unknown as Atom.Writable; @@ -1855,17 +1820,17 @@ export const collectionsAtom = queryAtom( { reactivityKeys: ["collections"] }, ).pipe(Atom.setIdleTTL("2 minutes")); -export interface GraphTriplesInput { - readonly flowId: string; - readonly collection: string; - readonly limit: number; -} +export class GraphTriplesInput extends S.Class("GraphTriplesInput")({ + flowId: S.String, + collection: S.String, + limit: S.Finite, +}, { description: "Workbench graph triples query atom input." }) {} -export interface ExplainTriplesInput { - readonly events: ExplainEvent[]; - readonly flowId: string; - readonly collection: string; -} +export class ExplainTriplesInput extends S.Class("ExplainTriplesInput")({ + events: S.Array(ClientExplainEvent).pipe(S.mutable), + flowId: S.String, + collection: S.String, +}, { description: "Workbench explain triples query atom input." }) {} const atomFamilyKeySeparator = "\u001f"; const explainGraphSeparator = "\u001e"; @@ -2139,13 +2104,13 @@ export const deleteMcpToolAtom = commandAtom("deleteMcpTool", Effe const chunkedUploadThreshold = 1_000_000; -export interface UploadDocumentInput { - readonly base64: string; - readonly mimeType: string; - readonly title: string; - readonly comments: string; - readonly tags: string[]; -} +export class UploadDocumentInput extends S.Class("UploadDocumentInput")({ + base64: S.String, + mimeType: S.String, + title: S.String, + comments: S.String, + tags: S.Array(S.String).pipe(S.mutable), +}, { description: "Workbench document upload command payload." }) {} const uploadDocumentEffect = Effect.fn("trustgraph.workbench.uploadDocument.effect")(function*( input: UploadDocumentInput, diff --git a/ts/packages/workbench/src/lib/graph-utils.ts b/ts/packages/workbench/src/lib/graph-utils.ts index c5494ad9..57bdf4e8 100644 --- a/ts/packages/workbench/src/lib/graph-utils.ts +++ b/ts/packages/workbench/src/lib/graph-utils.ts @@ -1,5 +1,6 @@ import type { Triple, Term } from "@trustgraph/client"; import { Match } from "effect"; +import * as S from "effect/Schema"; import type { ForceGraphProps, NodeObject, LinkObject } from "react-force-graph-2d"; // --------------------------------------------------------------------------- @@ -27,10 +28,23 @@ export interface GraphLink extends LinkObject { label: string; } -export interface GraphData { - nodes: GraphNode[]; - links: GraphLink[]; -} +const GraphNodeValue: S.Codec = S.Struct({ + id: S.String, + label: S.String, + color: S.optionalKey(S.String), + degree: S.Finite, +}); + +const GraphLinkValue: S.Codec = S.Struct({ + source: S.String, + target: S.String, + label: S.String, +}); + +export class GraphData extends S.Class("GraphData")({ + nodes: S.Array(GraphNodeValue).pipe(S.mutable), + links: S.Array(GraphLinkValue).pipe(S.mutable), +}, { description: "Renderable graph nodes and links derived from triples." }) {} export const DEFAULT_GRAPH_NODE_COLOR = "#82b582"; diff --git a/ts/packages/workbench/src/qa/initial-values.ts b/ts/packages/workbench/src/qa/initial-values.ts index ce99b489..8d0aabe6 100644 --- a/ts/packages/workbench/src/qa/initial-values.ts +++ b/ts/packages/workbench/src/qa/initial-values.ts @@ -11,14 +11,14 @@ import { settingsAtom, } from "@/atoms/workbench"; import type { BaseApi } from "@trustgraph/client"; -import type { MockWorkbenchFixture } from "@/qa/mock-api"; -import { makeMockBaseApi, qaSettingsFromFixture, } from "@/qa/mock-api"; +import { MockWorkbenchFixture, makeMockBaseApi, qaSettingsFromFixture, } from "@/qa/mock-api"; +import { Schema as S } from "effect"; -export interface WorkbenchQaWindowConfig { - readonly enabled?: boolean; - readonly fixture?: MockWorkbenchFixture; - readonly flowId?: string; -} +export class WorkbenchQaWindowConfig extends S.Class("WorkbenchQaWindowConfig")({ + enabled: S.optionalKey(S.Boolean), + fixture: S.optionalKey(MockWorkbenchFixture), + flowId: S.optionalKey(S.String), +}, { description: "Browser-provided workbench QA boot configuration." }) {} declare global { interface Window { diff --git a/ts/packages/workbench/src/qa/mock-api.ts b/ts/packages/workbench/src/qa/mock-api.ts index 05c8a299..75c8d9b0 100644 --- a/ts/packages/workbench/src/qa/mock-api.ts +++ b/ts/packages/workbench/src/qa/mock-api.ts @@ -4,42 +4,97 @@ import { Clock, Effect, Match, Option, Schema as S } from "effect"; type ConfigValues = Record>; -export interface MockWorkbenchFixture { - readonly settings?: { - readonly user?: string; - readonly apiKey?: string; - readonly gatewayUrl?: string; - readonly collection?: string; - readonly featureSwitches?: Record; - }; - readonly flows?: { - readonly activeIds?: string[]; - readonly definitions?: Record>; - readonly blueprints?: Record>; - }; - readonly config?: { - readonly prompt?: Record; - readonly valuesByType?: ConfigValues; - }; - readonly library?: { - readonly documents?: DocumentMetadata[]; - readonly processing?: ProcessingMetadata[]; - readonly metadataById?: Record; - }; - readonly knowledge?: { - readonly kgCores?: string[]; - readonly deCores?: string[]; - readonly loadedKgCores?: string[]; - }; - readonly collections?: Array>; - readonly graph?: { - readonly triplesByFlowCollection?: Record; - readonly explainTriplesByGraph?: Record; - }; - readonly chat?: { - readonly delayFrames?: number; - }; -} +const UnknownRecord = S.Record(S.String, S.Unknown); +const ConfigValuesRecord = S.Record(S.String, UnknownRecord); + +const ClientTerm: S.Codec = S.suspend(() => + S.Union([ + S.Struct({ t: S.Literal("i"), i: S.String }), + S.Struct({ t: S.Literal("b"), d: S.String }), + S.Struct({ + t: S.Literal("l"), + v: S.String, + dt: S.optionalKey(S.String), + ln: S.optionalKey(S.String), + }), + S.Struct({ + t: S.Literal("t"), + tr: S.optionalKey(ClientTriple), + }), + ]) +); + +const ClientTriple: S.Codec = S.suspend(() => + S.Struct({ + s: ClientTerm, + p: ClientTerm, + o: ClientTerm, + g: S.optionalKey(S.String), + }) +); + +const DocumentMetadataValue: S.Codec = S.Struct({ + id: S.optionalKey(S.String), + time: S.optionalKey(S.Finite), + kind: S.optionalKey(S.String), + title: S.optionalKey(S.String), + comments: S.optionalKey(S.String), + metadata: S.optionalKey(S.Array(ClientTriple).pipe(S.mutable)), + user: S.optionalKey(S.String), + tags: S.optionalKey(S.Array(S.String).pipe(S.mutable)), + parentId: S.optionalKey(S.String), + documentType: S.optionalKey(S.String), + "parent-id": S.optionalKey(S.String), + "document-type": S.optionalKey(S.String), +}); + +const ProcessingMetadataValue: S.Codec = S.Struct({ + id: S.optionalKey(S.String), + "document-id": S.optionalKey(S.String), + documentId: S.optionalKey(S.String), + time: S.optionalKey(S.Finite), + flow: S.optionalKey(S.String), + user: S.optionalKey(S.String), + collection: S.optionalKey(S.String), + tags: S.optionalKey(S.Array(S.String).pipe(S.mutable)), +}); + +export class MockWorkbenchFixture extends S.Class("MockWorkbenchFixture")({ + settings: S.optionalKey(S.Struct({ + user: S.optionalKey(S.String), + apiKey: S.optionalKey(S.String), + gatewayUrl: S.optionalKey(S.String), + collection: S.optionalKey(S.String), + featureSwitches: S.optionalKey(S.Record(S.String, S.Boolean)), + })), + flows: S.optionalKey(S.Struct({ + activeIds: S.optionalKey(S.Array(S.String).pipe(S.mutable)), + definitions: S.optionalKey(S.Record(S.String, UnknownRecord)), + blueprints: S.optionalKey(S.Record(S.String, UnknownRecord)), + })), + config: S.optionalKey(S.Struct({ + prompt: S.optionalKey(UnknownRecord), + valuesByType: S.optionalKey(ConfigValuesRecord), + })), + library: S.optionalKey(S.Struct({ + documents: S.optionalKey(S.Array(DocumentMetadataValue).pipe(S.mutable)), + processing: S.optionalKey(S.Array(ProcessingMetadataValue).pipe(S.mutable)), + metadataById: S.optionalKey(S.Record(S.String, DocumentMetadataValue)), + })), + knowledge: S.optionalKey(S.Struct({ + kgCores: S.optionalKey(S.Array(S.String).pipe(S.mutable)), + deCores: S.optionalKey(S.Array(S.String).pipe(S.mutable)), + loadedKgCores: S.optionalKey(S.Array(S.String).pipe(S.mutable)), + })), + collections: S.optionalKey(S.Array(UnknownRecord).pipe(S.mutable)), + graph: S.optionalKey(S.Struct({ + triplesByFlowCollection: S.optionalKey(S.Record(S.String, S.Array(ClientTriple).pipe(S.mutable))), + explainTriplesByGraph: S.optionalKey(S.Record(S.String, S.Array(ClientTriple).pipe(S.mutable))), + })), + chat: S.optionalKey(S.Struct({ + delayFrames: S.optionalKey(S.Finite), + })), +}, { description: "Seed fixture for deterministic workbench QA runs." }) {} interface UploadSession { readonly metadata: DocumentMetadata; diff --git a/ts/scripts/check-effect-laws.ts b/ts/scripts/check-effect-laws.ts index 16b84259..a66c5baf 100644 Binary files a/ts/scripts/check-effect-laws.ts and b/ts/scripts/check-effect-laws.ts differ diff --git a/ts/scripts/effect-laws.allowlist.json b/ts/scripts/effect-laws.allowlist.json index 822dc21f..57811ddf 100644 --- a/ts/scripts/effect-laws.allowlist.json +++ b/ts/scripts/effect-laws.allowlist.json @@ -1 +1 @@ -{"exemptions":[],"baseline":[{"rule":"no-effect-run","path":"packages/base/src/processor/async-processor.ts","count":1},{"rule":"no-effect-run","path":"packages/client/src/socket/effect-rpc-client.ts","count":10},{"rule":"no-effect-run","path":"packages/client/src/socket/trustgraph-socket.ts","count":9},{"rule":"no-effect-run","path":"packages/flow/src/agent/mcp-tool/service.ts","count":1},{"rule":"no-effect-run","path":"packages/flow/src/agent/react/service.ts","count":2},{"rule":"no-effect-run","path":"packages/flow/src/chunking/service.ts","count":1},{"rule":"no-effect-run","path":"packages/flow/src/decoding/pdf-decoder.ts","count":1},{"rule":"no-effect-run","path":"packages/flow/src/embeddings/ollama.ts","count":1},{"rule":"no-effect-run","path":"packages/flow/src/extract/knowledge-extract.ts","count":4},{"rule":"no-effect-run","path":"packages/flow/src/librarian/service.ts","count":1},{"rule":"no-effect-run","path":"packages/flow/src/model/text-completion/azure-openai.ts","count":1},{"rule":"no-effect-run","path":"packages/flow/src/model/text-completion/claude.ts","count":1},{"rule":"no-effect-run","path":"packages/flow/src/model/text-completion/mistral.ts","count":1},{"rule":"no-effect-run","path":"packages/flow/src/model/text-completion/ollama.ts","count":1},{"rule":"no-effect-run","path":"packages/flow/src/model/text-completion/openai-compatible.ts","count":1},{"rule":"no-effect-run","path":"packages/flow/src/model/text-completion/openai.ts","count":1},{"rule":"no-effect-run","path":"packages/flow/src/prompt/template.ts","count":1},{"rule":"no-effect-run","path":"packages/mcp/src/server-effect.ts","count":1},{"rule":"no-effect-run","path":"packages/workbench/src/atoms/workbench.ts","count":1},{"rule":"no-effect-run","path":"packages/workbench/src/components/error-boundary.tsx","count":1},{"rule":"no-effect-run","path":"packages/workbench/src/qa/mock-api.ts","count":1},{"rule":"no-error-throw","path":"packages/workbench/src/main.tsx","count":1},{"rule":"no-error-throw","path":"scripts/seed-config.ts","count":1},{"rule":"no-error-throw","path":"scripts/seed-demo.ts","count":4},{"rule":"no-error-throw","path":"scripts/seed-flows.ts","count":2},{"rule":"no-error-throw","path":"scripts/test-pipeline.ts","count":2},{"rule":"no-native-fetch","path":"scripts/seed-config.ts","count":1},{"rule":"no-native-fetch","path":"scripts/seed-demo.ts","count":11},{"rule":"no-native-fetch","path":"scripts/seed-flows.ts","count":1},{"rule":"no-native-fetch","path":"scripts/test-pipeline.ts","count":5},{"rule":"no-native-json","path":"scripts/seed-config.ts","count":6},{"rule":"no-native-json","path":"scripts/seed-demo.ts","count":6},{"rule":"no-native-json","path":"scripts/seed-flows.ts","count":3},{"rule":"no-native-json","path":"scripts/test-pipeline.ts","count":6},{"rule":"no-native-sort","path":"packages/client/src/socket/trustgraph-socket.ts","count":2},{"rule":"no-native-sort","path":"packages/flow/src/config/service.ts","count":3},{"rule":"no-native-sort","path":"packages/flow/src/cores/service.ts","count":1},{"rule":"no-native-sort","path":"packages/flow/src/flow-manager/service.ts","count":1},{"rule":"no-native-sort","path":"packages/flow/src/librarian/service.ts","count":1},{"rule":"no-native-sort","path":"packages/flow/src/retrieval/graph-rag.ts","count":1},{"rule":"no-native-sort","path":"packages/workbench/src/atoms/workbench.ts","count":2},{"rule":"no-native-sort","path":"packages/workbench/src/components/chat/explain-graph.tsx","count":1},{"rule":"no-native-sort","path":"packages/workbench/src/pages/graph.tsx","count":1},{"rule":"no-native-sort","path":"packages/workbench/src/qa/mock-api.ts","count":1},{"rule":"no-native-sort","path":"scripts/inventory-native-classes.ts","count":1},{"rule":"no-native-sort","path":"scripts/seed-demo.ts","count":1},{"rule":"no-native-sort","path":"scripts/seed-flows.ts","count":1},{"rule":"no-native-timers","path":"scripts/test-pipeline.ts","count":2},{"rule":"no-node-fs-path","path":"scripts/create-test-pdf.ts","count":1},{"rule":"no-node-fs-path","path":"scripts/inventory-native-classes.ts","count":2},{"rule":"no-process-env","path":"scripts/seed-config.ts","count":2},{"rule":"no-process-env","path":"scripts/seed-demo.ts","count":5},{"rule":"no-process-env","path":"scripts/seed-flows.ts","count":1},{"rule":"no-process-env","path":"scripts/test-pipeline.ts","count":11},{"rule":"no-schema-suffix","path":"packages/base/src/schema/primitives.ts","count":1},{"rule":"schema-first-data","path":"packages/base/src/backend/types.ts","count":2},{"rule":"schema-first-data","path":"packages/base/src/messaging/consumer.ts","count":2},{"rule":"schema-first-data","path":"packages/base/src/messaging/request-response.ts","count":1},{"rule":"schema-first-data","path":"packages/base/src/messaging/runtime.ts","count":4},{"rule":"schema-first-data","path":"packages/base/src/metrics/prometheus.ts","count":1},{"rule":"schema-first-data","path":"packages/base/src/processor/async-processor.ts","count":1},{"rule":"schema-first-data","path":"packages/base/src/processor/flow.ts","count":2},{"rule":"schema-first-data","path":"packages/base/src/runtime/config.ts","count":1},{"rule":"schema-first-data","path":"packages/base/src/runtime/messaging-config.ts","count":1},{"rule":"schema-first-data","path":"packages/base/src/schema/primitives.ts","count":2},{"rule":"schema-first-data","path":"packages/cli/src/commands/util.ts","count":1},{"rule":"schema-first-data","path":"packages/client/src/models/Triple.ts","count":6},{"rule":"schema-first-data","path":"packages/client/src/models/messages.ts","count":58},{"rule":"schema-first-data","path":"packages/client/src/socket/effect-rpc-client.ts","count":3},{"rule":"schema-first-data","path":"packages/client/src/socket/trustgraph-socket.ts","count":5},{"rule":"schema-first-data","path":"packages/flow/src/agent/react/tools.ts","count":1},{"rule":"schema-first-data","path":"packages/flow/src/agent/react/types.ts","count":2},{"rule":"schema-first-data","path":"packages/flow/src/config/service.ts","count":1},{"rule":"schema-first-data","path":"packages/flow/src/cores/service.ts","count":1},{"rule":"schema-first-data","path":"packages/flow/src/embeddings/ollama.ts","count":1},{"rule":"schema-first-data","path":"packages/flow/src/gateway/rpc-server.ts","count":1},{"rule":"schema-first-data","path":"packages/flow/src/gateway/server.ts","count":1},{"rule":"schema-first-data","path":"packages/flow/src/librarian/collection-manager.ts","count":1},{"rule":"schema-first-data","path":"packages/flow/src/librarian/service.ts","count":1},{"rule":"schema-first-data","path":"packages/flow/src/model/text-completion/common.ts","count":1},{"rule":"schema-first-data","path":"packages/flow/src/prompt/template.ts","count":2},{"rule":"schema-first-data","path":"packages/flow/src/qdrant/client.ts","count":4},{"rule":"schema-first-data","path":"packages/flow/src/query/embeddings/qdrant-doc.ts","count":3},{"rule":"schema-first-data","path":"packages/flow/src/query/embeddings/qdrant-graph.ts","count":3},{"rule":"schema-first-data","path":"packages/flow/src/query/triples/falkordb.ts","count":2},{"rule":"schema-first-data","path":"packages/flow/src/retrieval/document-rag.ts","count":2},{"rule":"schema-first-data","path":"packages/flow/src/retrieval/graph-rag.ts","count":4},{"rule":"schema-first-data","path":"packages/flow/src/storage/embeddings/qdrant-doc.ts","count":3},{"rule":"schema-first-data","path":"packages/flow/src/storage/embeddings/qdrant-graph.ts","count":3},{"rule":"schema-first-data","path":"packages/flow/src/storage/triples/falkordb.ts","count":2},{"rule":"schema-first-data","path":"packages/mcp/src/server-effect.ts","count":2},{"rule":"schema-first-data","path":"packages/workbench/src/atoms/workbench.ts","count":21},{"rule":"schema-first-data","path":"packages/workbench/src/lib/graph-utils.ts","count":3},{"rule":"schema-first-data","path":"packages/workbench/src/qa/initial-values.ts","count":1},{"rule":"schema-first-data","path":"packages/workbench/src/qa/mock-api.ts","count":1}]} +{"exemptions":[],"baseline":[{"rule":"no-effect-run","path":"packages/base/src/processor/async-processor.ts","count":1},{"rule":"no-effect-run","path":"packages/client/src/socket/effect-rpc-client.ts","count":10},{"rule":"no-effect-run","path":"packages/client/src/socket/trustgraph-socket.ts","count":9},{"rule":"no-effect-run","path":"packages/flow/src/agent/mcp-tool/service.ts","count":1},{"rule":"no-effect-run","path":"packages/flow/src/agent/react/service.ts","count":2},{"rule":"no-effect-run","path":"packages/flow/src/chunking/service.ts","count":1},{"rule":"no-effect-run","path":"packages/flow/src/decoding/pdf-decoder.ts","count":1},{"rule":"no-effect-run","path":"packages/flow/src/embeddings/ollama.ts","count":1},{"rule":"no-effect-run","path":"packages/flow/src/extract/knowledge-extract.ts","count":4},{"rule":"no-effect-run","path":"packages/flow/src/librarian/service.ts","count":1},{"rule":"no-effect-run","path":"packages/flow/src/model/text-completion/azure-openai.ts","count":1},{"rule":"no-effect-run","path":"packages/flow/src/model/text-completion/claude.ts","count":1},{"rule":"no-effect-run","path":"packages/flow/src/model/text-completion/mistral.ts","count":1},{"rule":"no-effect-run","path":"packages/flow/src/model/text-completion/ollama.ts","count":1},{"rule":"no-effect-run","path":"packages/flow/src/model/text-completion/openai-compatible.ts","count":1},{"rule":"no-effect-run","path":"packages/flow/src/model/text-completion/openai.ts","count":1},{"rule":"no-effect-run","path":"packages/flow/src/prompt/template.ts","count":1},{"rule":"no-effect-run","path":"packages/mcp/src/server-effect.ts","count":1},{"rule":"no-effect-run","path":"packages/workbench/src/atoms/workbench.ts","count":1},{"rule":"no-effect-run","path":"packages/workbench/src/components/error-boundary.tsx","count":1},{"rule":"no-effect-run","path":"packages/workbench/src/qa/mock-api.ts","count":1},{"rule":"no-error-throw","path":"packages/workbench/src/main.tsx","count":1},{"rule":"no-error-throw","path":"scripts/seed-config.ts","count":1},{"rule":"no-error-throw","path":"scripts/seed-demo.ts","count":4},{"rule":"no-error-throw","path":"scripts/seed-flows.ts","count":2},{"rule":"no-error-throw","path":"scripts/test-pipeline.ts","count":2},{"rule":"no-native-fetch","path":"scripts/seed-config.ts","count":1},{"rule":"no-native-fetch","path":"scripts/seed-demo.ts","count":11},{"rule":"no-native-fetch","path":"scripts/seed-flows.ts","count":1},{"rule":"no-native-fetch","path":"scripts/test-pipeline.ts","count":5},{"rule":"no-native-json","path":"scripts/seed-config.ts","count":6},{"rule":"no-native-json","path":"scripts/seed-demo.ts","count":6},{"rule":"no-native-json","path":"scripts/seed-flows.ts","count":3},{"rule":"no-native-json","path":"scripts/test-pipeline.ts","count":6},{"rule":"no-native-sort","path":"packages/client/src/socket/trustgraph-socket.ts","count":2},{"rule":"no-native-sort","path":"packages/flow/src/config/service.ts","count":3},{"rule":"no-native-sort","path":"packages/flow/src/cores/service.ts","count":1},{"rule":"no-native-sort","path":"packages/flow/src/flow-manager/service.ts","count":1},{"rule":"no-native-sort","path":"packages/flow/src/librarian/service.ts","count":1},{"rule":"no-native-sort","path":"packages/flow/src/retrieval/graph-rag.ts","count":1},{"rule":"no-native-sort","path":"packages/workbench/src/atoms/workbench.ts","count":2},{"rule":"no-native-sort","path":"packages/workbench/src/components/chat/explain-graph.tsx","count":1},{"rule":"no-native-sort","path":"packages/workbench/src/pages/graph.tsx","count":1},{"rule":"no-native-sort","path":"packages/workbench/src/qa/mock-api.ts","count":1},{"rule":"no-native-sort","path":"scripts/inventory-native-classes.ts","count":1},{"rule":"no-native-sort","path":"scripts/seed-demo.ts","count":1},{"rule":"no-native-sort","path":"scripts/seed-flows.ts","count":1},{"rule":"no-native-timers","path":"scripts/test-pipeline.ts","count":2},{"rule":"no-node-fs-path","path":"scripts/create-test-pdf.ts","count":1},{"rule":"no-node-fs-path","path":"scripts/inventory-native-classes.ts","count":2},{"rule":"no-process-env","path":"scripts/seed-config.ts","count":2},{"rule":"no-process-env","path":"scripts/seed-demo.ts","count":5},{"rule":"no-process-env","path":"scripts/seed-flows.ts","count":1},{"rule":"no-process-env","path":"scripts/test-pipeline.ts","count":11},{"rule":"no-schema-suffix","path":"packages/base/src/schema/primitives.ts","count":1},{"rule":"schema-first-data","path":"packages/client/src/models/Triple.ts","count":6},{"rule":"schema-first-data","path":"packages/client/src/models/messages.ts","count":58},{"rule":"schema-first-data","path":"packages/client/src/socket/effect-rpc-client.ts","count":2},{"rule":"schema-first-data","path":"packages/client/src/socket/trustgraph-socket.ts","count":4}]}