chore(ts): consolidate effect native closeout

This commit is contained in:
elpresidank 2026-06-11 08:34:59 -05:00
parent cd6c9107d7
commit fab718dce8
21 changed files with 199 additions and 3533 deletions

View file

@ -1,151 +0,0 @@
# TrustGraph TS Native Class To Functional Effect Services Goal
Use the `grill-me` skill from:
`/home/elpresidank/YeeBois/projects/beep-effect/.agents/skills/grill-me/SKILL.md`
## Objective
Refactor the TrustGraph TypeScript port so production runtime code no longer
uses native TypeScript or JavaScript class syntax except where an Effect module
API truly requires class syntax and no practical functional form exists.
The preferred style is functional and pure:
- Functions, factories, closures, and object-literal service implementations.
- Functions returning functions or service objects instead of constructors.
- `Context.Service` tags plus `Layer` builders for dependency provision.
- `Effect.gen`, `Effect.fn`, `Effect.acquireRelease`, `Effect.addFinalizer`,
and scoped Layers for lifecycle.
- Top-level `Effect.runPromise` only at CLI/bootstrap boundaries.
Terminal condition: repeated inventory agents find zero blocking native classes
in production runtime source like these current smells:
- `ts/packages/base/src/processor/async-processor.ts#L38-162`
- `ts/packages/base/src/processor/flow-processor.ts#L284-358`
## Class Policy
Blocking scope is production runtime source under `ts/packages/**/src`,
excluding `__tests__`, `*.test.ts`, and `*.spec.ts`.
Tests and scripts must still be inventoried separately, but they do not block
completion unless they are required to verify migrated production behavior.
Allowed class syntax is intentionally narrow and proof-based. Do not preserve
class syntax just because it is Effect-adjacent. For every candidate exemption,
prove that the specific Effect API requires class syntax and that a functional
alternative is not better.
Candidate exemptions to investigate, not blindly preserve:
- `S.Class`, `S.TaggedClass`, `S.TaggedErrorClass`, `S.ErrorClass`
- `Data.TaggedError`
- `Context.Service`
- Effect RPC / HTTP API class forms such as `Rpc.make` and `HttpApi.make`
If a functional Effect equivalent exists, use the functional form. The target
style is functions returning functions or service objects, not class-shaped
Effect code.
Blocking class forms include:
- Abstract base classes and inheritance-based processors.
- Service classes extending `AsyncProcessor`, `FlowProcessor`, or `LlmService`.
- Client API wrapper classes.
- Backend adapter classes.
- Spec/resource classes.
- Query/store/engine classes.
- Metrics/parser/lifecycle classes.
- React class components such as error boundaries.
## Required Workflow
1. Read and follow `grill-me`.
2. Before asking the user questions, use parallel read-only sub-agents to
inventory class declarations.
3. Use AST-based inspection where possible; regex alone is not enough.
4. Split inventory agents by subsystem:
- Base processor/messaging/runtime/specs/backend.
- Flow service processors and LLM/embedding/RAG services.
- Query/store engines and adapters.
- Client socket/API wrappers.
- Workbench production source.
5. Consolidate findings into:
- Blocking production native classes.
- Effect-native class forms with proof.
- Functional replacements available for prior exemptions.
- Non-blocking test/script classes.
6. Use `grill-me` to resolve any remaining API or rollout tradeoffs.
7. Produce a decision-complete implementation plan.
8. After approval, implement in phases and rerun inventory until blocking count
is zero.
## Refactor Direction
Replace inheritance and mutable class instances with:
- Plain TypeScript service interfaces.
- `Context.Service` tags only where needed for Effect dependency injection.
- `Layer.succeed`, `Layer.effect`, and `Layer.scoped` for construction.
- Closure-held state only where necessary, preferably scoped and finalized.
- Object-literal services instead of class instances.
- Factory functions like `makeXService`, `makeXLayer`, and `runX`.
- `Effect.tryPromise` for external Promise APIs.
- `Effect.fn` for reusable effectful service methods.
Preserve behavior and in-repo call semantics, but do not preserve native class
constructors as public API. Replace exported classes with interfaces, service
tags, factory functions, or layer builders, and update all in-repo callers.
## Verification Requirements
Required gates:
- `cd ts && bun run check:tsgo`
- `cd ts && bun run build`
- `cd ts && bun run test`
- `cd ts && bun run workbench:qa`
- Add or provide an inventory command/script that fails on blocking production
classes.
- Run the inventory command after every migration phase.
- Final inventory must report zero blocking production native classes.
Live smoke if service surfaces changed:
- `cd ts/deploy && docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build`
- `cd ts && bun run seed`
- `cd ts && bun run seed:demo`
- `cd ts && bun run seed:flows`
- `cd ts && SKIP_LLM=1 bun run test:pipeline`
## New Session Prompt
Paste this into a new Codex session:
```text
Use the grill-me skill from /home/elpresidank/YeeBois/projects/beep-effect/.agents/skills/grill-me/SKILL.md.
The repo is /home/elpresidank/YeeBois/dev/trustgraph and the TypeScript port is in ./ts. I want to remove native TypeScript/JavaScript class syntax from production runtime code and convert the port to functional, pure Effect services.
Before asking me questions, use parallel read-only sub-agents to inventory every class declaration under ts/packages/**/src. Production runtime source is the blocking scope; tests and scripts should be inventoried separately but are non-blocking. Use AST-based inspection where possible, not regex alone.
Allowed class syntax is extremely narrow and proof-based. Even Effect-native-looking classes such as S.Class, S.TaggedClass, S.TaggedErrorClass, S.ErrorClass, Data.TaggedError, Context.Service, Rpc.make, and HttpApi.make must be treated as candidate exemptions, not automatic exemptions. If a functional Effect equivalent exists, use the functional form. I prefer functions, factories, closures, object-literal service implementations, Context.Service tags, and Layers over all class-shaped code.
Use these files as representative smells:
- ts/packages/base/src/processor/async-processor.ts#L38-162
- ts/packages/base/src/processor/flow-processor.ts#L284-358
Inventory by subsystem: base processor/messaging/runtime/specs/backend; flow service processors and LLM/embedding/RAG services; query/store engines and adapters; client socket/API wrappers; workbench production source.
After inventory, use grill-me to resolve any remaining API or rollout tradeoffs, then produce a decision-complete implementation plan. After I approve the plan, implement in phases and repeat inventory until zero blocking production native classes remain.
Required verification: cd ts && bun run check:tsgo; cd ts && bun run build; cd ts && bun run test; cd ts && bun run workbench:qa; add or provide an inventory command/script that fails on blocking production classes. If service surfaces changed, also run the live docker/seed/pipeline smoke from this file.
```
## Goal Command
```text
/goal Use ts/CLASS_EFFECT_GOAL.md to run the TrustGraph TS native-class-to-functional-Effect-services migration. First use grill-me and parallel read-only sub-agents to inventory every native class declaration. Then produce and execute a decision-complete refactor plan that replaces classes with pure functions, factories, closures, object-literal services, Context.Service tags, and Layers. Terminal condition: repeated inventory agents and the repo inventory command report zero blocking native classes in production runtime source under ts/packages/**/src. Even Effect-native class forms such as S.Class, TaggedError classes, Context.Service, Rpc.make, and HttpApi.make must be treated as narrow proof-based exemptions; if a functional Effect equivalent exists, use it. Verify with check:tsgo, build, test, workbench:qa, and live smoke when service surfaces change. Preserve unrelated local files.
```

File diff suppressed because it is too large Load diff

View file

@ -1,127 +0,0 @@
# TrustGraph TS Effect-Native Rewrite Plan
## Summary
Bring the TypeScript port to a genuinely Effect-native shape while keeping existing TrustGraph capabilities working. The current native MCP rewrite should be preserved. The remaining work is to remove Promise-first TrustGraph APIs and replace manual host/framework plumbing with Effect v4-native backend, messaging, processor, HTTP/RPC, and CLI layers.
The implementation should proceed in green checkpoints rather than one broad sweep. Each stage should leave the repo closer to Effect-native and should run the smallest relevant gate before moving on.
## Non-Negotiable Architecture Decisions
- TrustGraph-authored production APIs should be Effect-first, not Promise-first.
- Remove exported Promise APIs from backend, messaging, processor, flow service, gateway, CLI, and service runner surfaces.
- Do not keep dual Promise and Effect methods on the same TrustGraph objects.
- Do not rely on Fastify, `@fastify/websocket`, Commander, or production `Effect.runPromise` / `ManagedRuntime` compatibility bridges.
- External JavaScript SDKs that are inherently Promise-based may be wrapped with `Effect.tryPromise`, but that Promise shape must stay behind Effect APIs.
- Gateway HTTP and WebSocket behavior should move to Effect v4 native modules: `effect/unstable/httpapi`, `effect/unstable/http`, `effect/unstable/rpc`, `effect/unstable/socket`, and platform Bun/Node HTTP/socket layers.
- CLI behavior should move to `effect/unstable/cli`.
- The MCP package should stay on the native `effect/unstable/ai/McpServer` implementation and must not reintroduce server-side `@modelcontextprotocol/sdk` or `zod`.
## Current Grounding
- `bun run check:tsgo` is green.
- `bun run build` is green.
- `bun run --cwd packages/mcp test` is green.
- `bun run test` currently fails only in `@trustgraph/base`, in `packages/base/src/__tests__/nats-backend.test.ts`.
- The NATS failures are a symptom of the backend still sitting between old Promise-shaped abstractions and newer JetStream calls. The rewrite should fix the contract, not only update the mock.
- The Effect v4 source of truth for APIs and examples is available at `/home/elpresidank/YeeBois/projects/beep-effect/.repos/effect-v4`.
## Stage 1: Backend And NATS
- Redefine `Message`, `BackendProducer`, `BackendConsumer`, and `PubSubBackend` around `Effect.Effect` return values.
- Make NATS connection, JetStream manager/client, stream initialization, and durable consumer handles scoped resources.
- Replace deprecated receive plumbing with modern JetStream consumer APIs.
- Preserve behavior for stream creation, durable consumer creation, headers, JSON/schema encode/decode, receive timeout returning `null`, ack/nak, close/drain, and tagged `PubSubError` mapping.
- Update NATS tests and in-memory fake backends to the new Effect-native interface.
Verification:
- `bun run --cwd packages/base test src/__tests__/nats-backend.test.ts`
- `bun run --cwd packages/base test`
- `bun run check:tsgo`
## Stage 2: Messaging And Processor Runtime
- Remove Promise facades from `makeProducer`, `makeConsumer`, `makeRequestResponse`, processor `start` / `stop` / `run`, and flow compatibility wrappers.
- Expose Effect values, scoped constructors, Context services, Layers, and finalizers as the only TrustGraph API shape.
- Convert message handlers, config handlers, shutdown callbacks, producer send/flush/close, consumer receive/ack/nak/close, and request/response operations to Effect functions.
- Remove internal `ManagedRuntime.make(Layer.empty)` compatibility runtimes from production source.
Verification:
- `bun run --cwd packages/base test`
- `bun run check:tsgo`
## Stage 3: Flow Service Call Sites
- Convert flow service state methods that currently call `Effect.runPromise` into Effect-valued service methods.
- Keep public feature behavior unchanged for config, flow manager, librarian, knowledge cores, retrieval, embeddings, triples, agent, MCP tool, and text-completion services.
- Convert test fakes to the same Effect-native backend/messaging contracts.
Verification:
- `bun run --cwd packages/flow test`
- `bun run test`
- `bun run check:tsgo`
## Stage 4: Gateway HTTP And RPC
- Remove Fastify and `@fastify/websocket` from `packages/flow`.
- Rebuild the gateway with Effect v4 `HttpApi` groups/endpoints and native RPC/socket layers.
- Preserve existing behavior:
- `POST /api/v1/workbench/dispatch`
- `POST /api/v1/:kind`
- `POST /api/v1/flow/:flow/service/:kind`
- `POST /api/v1/flow/:flow/load`
- `GET /api/v1/rpc`
- `GET /api/v1/metrics`
- bearer auth behavior and RPC token behavior
- existing response/error shapes expected by clients and workbench
- Use native Effect HTTP test utilities or route-level protocol tests instead of Fastify injection.
Verification:
- `bun run --cwd packages/flow test`
- `bun run --cwd packages/client test`
- `bun run test`
- `bun run build`
## Stage 5: CLI And Runner Scripts
- Remove Commander from `packages/cli`.
- Rebuild CLI commands with `effect/unstable/cli` and Effect-native socket/API clients.
- Convert service runner scripts to launch Effect programs directly with platform runtime `runMain` style execution.
- Remove `run(): Promise<void>` exports from flow services; export Effect programs/layers and Effect-native `runMain` helpers instead.
- Leave script-only demo/seed tools as follow-up only if they are outside production package source, but do not let production packages depend on Promise facades.
Verification:
- `bun run --cwd packages/cli test`
- `bun run check:tsgo`
- `bun run build`
- `bun run test`
## Stage 6: Cleanup And Acceptance
- Remove obsolete dependencies from package manifests and lockfile:
- `fastify`
- `@fastify/websocket`
- `commander`
- any legacy dependency made unnecessary by the rewrite
- Preserve unrelated dirty work. Do not revert user changes.
- Use parallel agents for bounded audits or disjoint rewrite slices when useful.
- Use Graphiti memory if available; if unavailable, continue safely and report it skipped.
Final verification:
- `bun run check:tsgo`
- `bun run build`
- `bun run lint`
- `bun run test`
- `bun run --cwd packages/mcp test`
- `bun run workbench:qa` after installing the matching Playwright browser if needed
- `git diff --check`
- `rg -n "fastify|@fastify/websocket|commander" packages package.json bun.lock` has no production dependency/use hits
- `rg -n "Effect\\.runPromise|ManagedRuntime\\.make|Promise<|async function main" packages/base/src packages/flow/src packages/cli/src scripts -g "*.ts"` has no production-source hits except tests or unavoidable external type declarations that are not TrustGraph APIs
Do not mark the goal complete until all required gates are green, or until a real external blocker is reported with the exact failing command, error, and smallest next action.

View file

@ -1,225 +0,0 @@
# TrustGraph Effect-Native Rewrite Playbook
This playbook is the context packet for read-only sub-agents that audit the
TrustGraph TypeScript port for code that hand-rolls behavior already provided by
Effect v4. It is not an implementation plan for a single rewrite. Its job is to
make future scouts fast, grounded, and allergic to invented APIs.
## Source Baseline
Verify these paths at the start of every audit run:
- TrustGraph TS port: `/home/elpresidank/YeeBois/dev/trustgraph/ts`
- Effect v4 subtree: `/home/elpresidank/YeeBois/projects/beep-effect2/.repos/effect-v4`
- Installed Effect fallback: `ts/node_modules/effect`
- Installed Atom React fallback: `ts/packages/workbench/node_modules/@effect/atom-react`
- Native TypeScript checker: `ts/node_modules/.bin/tsgo`
The prompt typo path `~/YeeBois/projecects/...` is not valid on this machine.
Use `~/YeeBois/projects/...`.
When a package is not present in the subtree, verify it from TrustGraph's
installed package sources before proposing a replacement. This matters for
`effect/unstable/reactivity`, `effect/unstable/ai`, `effect/unstable/rpc`,
`effect/unstable/socket`, `effect/unstable/http`, `effect/unstable/httpapi`,
and `@effect/atom-react`.
Important import rule: the Effect v4 subtree contains separate packages such as
`@effect/ai`, `@effect/rpc`, and `@effect/platform`, but TrustGraph currently
resolves many beta APIs through `effect/unstable/*` import paths. Prefer the
installed TrustGraph import path when it exists; use the subtree package path as
source proof, not as an automatic import recommendation.
TrustGraph's TypeScript check must use `@typescript/native-preview` patched by
`@effect/tsgo`. The expected local version line is currently:
```sh
cd ts && ./node_modules/.bin/tsgo --version
# Version 7.0.0-dev+effect-tsgo.0.13.0
```
The root `ts/tsconfig.base.json` language-service plugin should stay aligned
with `/home/elpresidank/YeeBois/projects/beep-effect/tsconfig.base.json`,
using the same diagnostic severities and no test-only diagnostic downgrades.
The repo-specific `namespaceImportPackages` entry should use `@trustgraph/*`.
The root `check` script should delegate to `check:tsgo`, and `check:tsgo` must
run `effect-tsgo patch` before `tsgo -b` so agents get Effect diagnostics
instead of plain TypeScript. Treat every `effect(...)` diagnostic as a hard
blocker.
## Primitive Map
Use this map as the starting baseline. A finding is only valid when it cites the
TrustGraph path, the import path, and the Effect source path that proves the
primitive exists.
| Handrolled pattern | Preferred Effect primitive | Import path | Verified source |
| --- | --- | --- | --- |
| Promise loops, top-level async orchestration | `Effect`, `Effect.fn`, `Effect.scoped`, `Effect.runPromiseWith` at boundaries | `effect` | `packages/effect/src/Effect.ts` |
| Resource construction and teardown | `Layer`, `Scope`, `Effect.acquireRelease`, `Effect.addFinalizer` | `effect` | `packages/effect/src/Layer.ts`, `packages/effect/src/Scope.ts` |
| Mutable service state | `Ref`, `SynchronizedRef`, `SubscriptionRef` | `effect` | `packages/effect/src/Ref.ts`, `packages/effect/src/SynchronizedRef.ts` |
| Long-lived keyed state and set membership | `HashMap`, `MutableHashMap`, `HashSet`, `MutableHashSet` | `effect` | `packages/effect/src/HashMap.ts`, `packages/effect/src/MutableHashMap.ts`, `packages/effect/src/HashSet.ts`, `packages/effect/src/MutableHashSet.ts` |
| Polling, delays, retry/backoff | `Schedule`, `Effect.sleep`, `Effect.retry` | `effect` | `packages/effect/src/Schedule.ts`, `packages/effect/src/Effect.ts` |
| Callback queues and streaming fanout | `Queue`, `PubSub`, `Stream`, `Channel` | `effect` | `packages/effect/src/Queue.ts`, `packages/effect/src/PubSub.ts`, `packages/effect/src/Stream.ts`, `packages/effect/src/Channel.ts` |
| Env/config decoding | `Config`, `ConfigProvider`, platform config providers | `effect`, `effect/ConfigProvider`, provider packages | `packages/effect/src/Config.ts`, `packages/effect/src/ConfigProvider.ts`, `packages/platform/src/PlatformConfigProvider.ts` |
| JSON/wire schemas | `Schema`, `ParseResult`, `RpcSchema` | `effect/Schema`, `effect/unstable/rpc/RpcSchema` | `packages/effect/src/Schema.ts`, `ts/node_modules/effect/src/unstable/rpc/RpcSchema.ts` |
| WebSocket lifecycle and framing | `Socket`, `RpcClient`, `RpcServer`, `RpcSerialization` | `effect/unstable/socket`, `effect/unstable/rpc` | `ts/node_modules/effect/src/unstable/socket/*.ts`, `ts/node_modules/effect/src/unstable/rpc/*.ts` |
| HTTP servers/clients and typed APIs | `HttpApi`, `HttpApiClient`, `HttpApiBuilder`, `HttpClient`, `HttpServer` | `effect/unstable/http`, `effect/unstable/httpapi`, platform providers | `ts/node_modules/effect/src/unstable/http/*.ts`, `ts/node_modules/effect/src/unstable/httpapi/*.ts` |
| File, storage, and process IO | `FileSystem`, `KeyValueStore`, `ChildProcess`, `ChildProcessSpawner` | `effect/FileSystem`, `effect/unstable/persistence/KeyValueStore`, `effect/unstable/process/*`, provider packages | `ts/node_modules/effect/src/FileSystem.ts`, `ts/node_modules/effect/src/unstable/persistence/KeyValueStore.ts`, `ts/node_modules/effect/src/unstable/process/*.ts` |
| Browser local storage and clipboard | `BrowserKeyValueStore`, `Clipboard`, `BrowserHttpClient`, `BrowserSocket` | `@effect/platform-browser/*` | `ts/node_modules/.bun/@effect+platform-browser@4.0.0-beta.75+a5c1409dbf4ddafe/node_modules/@effect/platform-browser/src/*.ts` |
| AI tools, MCP, and model calls | `Tool`, `Toolkit`, `McpServer`, `McpSchema`, `LanguageModel`, provider layers | `effect/unstable/ai`, provider packages such as `@effect/ai-openai` | `ts/node_modules/effect/src/unstable/ai/*.ts`, `packages/ai/ai/src/*.ts` |
| Workbench async state | `Atom`, `AtomRpc`, `AtomHttpApi`, `AsyncResult`, `AtomRegistry`, `Reactivity` | `effect/unstable/reactivity`, `@effect/atom-react` | `ts/node_modules/effect/src/unstable/reactivity/*.ts`, `ts/packages/workbench/node_modules/@effect/atom-react/src/*.ts` |
| Metrics and logs | `Metric`, `Logger`, `Effect.log*` | `effect`, `@effect/opentelemetry` | `packages/effect/src/Metric.ts`, `packages/effect/src/Logger.ts` |
| Normal internal errors | `S.TaggedErrorClass` and existing TrustGraph tagged errors | `effect/Schema`, `@trustgraph/base/errors` | `packages/effect/src/Schema.ts`, `ts/packages/base/src/errors.ts` |
| Imperative exception capture | `Effect.try`, `Effect.tryPromise`, or `Result.try` | `effect`, `effect/Result` | `packages/effect/src/Effect.ts`, `packages/effect/src/Result.ts` |
Known concrete exports useful to scouts:
- `Socket.makeWebSocket`, `Socket.fromWebSocket`, `Socket.toChannel`,
`Socket.toChannelString`, `Socket.layerWebSocket`.
- `RpcClient.layerProtocolSocket`, `RpcServer.layerProtocolWebsocket`,
`RpcServer.layerProtocolSocketServer`, `RpcSerialization.layerNdjson`,
`RpcSerialization.layerNdJsonRpc`.
- `BunFileSystem`, `BunSocket`, `BunHttpClient`, `BunHttpServer`,
`BunRuntime`, `BunChildProcessSpawner` from `@effect/platform-bun`.
- `BrowserKeyValueStore.layerLocalStorage`,
`BrowserKeyValueStore.layerSessionStorage`,
`BrowserHttpClient.layerXMLHttpRequest`.
- `Atom.make`, `Atom.runtime`, `Atom.fn`, `Atom.family`,
`Atom.subscriptionRef`, `AtomRegistry.layer`, `Reactivity.layer`,
`Reactivity.query`, `Reactivity.stream`, `Reactivity.mutation`.
- `Tool.make`, `Toolkit.make`, `McpServer.registerToolkit`,
`LanguageModel.generateText`, `LanguageModel.streamText`.
- `S.TaggedErrorClass` for internal/library errors. Treat `new Error` inside
library internals as migration evidence unless it is a host/tool boundary,
test-only helper, or externally mandated error shape.
- `Effect.try`, `Effect.tryPromise`, and `Result.try` for exception capture.
Treat `try`/`catch` blocks as migration evidence unless they are host/tool
boundaries or test-only helpers.
- `S.toTaggedUnion(...).match` and `effect/Match` discriminator helpers for
discriminated unions. Treat native `switch` statements as migration evidence
unless the code is a host parser/traversal boundary with no useful Effect
schema or match primitive.
## Scout Workflow
1. Confirm repo state with `git status -sb`. Preserve unrelated files,
especially `.idea/effect.intellij.xml`.
2. Refresh the source baseline above. If a path moved, record the corrected path
in the report before making any recommendations.
3. Verify the tsgo baseline:
```sh
cd ts && ./node_modules/.bin/tsgo --version
bun run check
```
4. Run quick signal scans:
```sh
rg -n "S\\.ErrorClass|try \\{|catch \\(|new Error|new Promise|setTimeout|while \\(|switch \\(|receive\\(|Effect\\.runPromise|toPromiseRequestor|makeAsyncProcessor|process\\.env|JSON\\.parse|JSON\\.stringify|localStorage|new Map|new Set|Map<|Set<|WebSocket" ts/packages --glob '*.ts' --glob '*.tsx'
```
5. Split scouts by lane. If the thread cannot spawn every scout in parallel,
run them in batches using the same report schema.
6. Every finding must include both:
- Evidence of the handrolled TrustGraph pattern.
- Evidence of the exact Effect primitive that could replace it.
7. Do not rewrite code in this audit. The output is a ranked opportunity map and
a recommended PR order.
## Agent Lanes
Use these lane prompts as the durable starting point.
### Base Messaging And Processor Runtime
Inspect `ts/packages/base/src/messaging`, `ts/packages/base/src/processor`, and
`ts/packages/base/src/spec`. Find Promise compatibility facades, polling
receivers, manual request/response caches, top-level `Effect.runPromise`, and
mutable maps. Compare against `Queue`, `PubSub`, `Stream`, `Scope`, `Layer`,
`Schedule`, `Ref`, and `SynchronizedRef`.
### Flow Stateful Services
Inspect `ts/packages/flow/src/config`, `librarian`, `cores`, `flow-manager`,
`prompt`, and service entrypoints. Find `makeAsyncProcessor` object services,
`while (this.running)`, `sleep`, `receive(2000)`, local persistence, and
process-env config. Compare against scoped layers, state refs, schedules,
`FileSystem`, `KeyValueStore`, `Config`, and schema codecs. In TrustGraph's
installed beta, prefer `effect/FileSystem` and
`effect/unstable/persistence/KeyValueStore` plus runtime-specific provider
packages.
### Gateway And RPC Boundaries
Inspect `ts/packages/flow/src/gateway` and `ts/packages/client/src/socket`.
Find manual requestor caches, streaming completion detectors, WebSocket
constructor shims, Promise-returning compatibility APIs, and repeated
`Effect.runPromise`. Compare against `Socket`, `RpcClient`, `RpcServer`,
`RpcSerialization`, `Stream`, `Queue`, and `Scope`.
### RAG, Agent, Provider, And Storage Layers
Inspect `ts/packages/flow/src/retrieval`, `agent`, `storage`, `query`, `model`,
and `embeddings`. Find `toPromiseRequestor`, direct SDK resource management,
ambient config, JSON parsing, and manual telemetry. Compare against
`EffectRequestResponse`, `Stream`, provider `Layer`s, `Config`, `Schema`,
`Metric`, `Logger`, and `effect/unstable/ai`.
### MCP And Workbench
Inspect `ts/packages/mcp` and `ts/packages/workbench`. Treat these as
lower-priority unless a handrolled pattern clearly remains. Prefer making the
Effect MCP server canonical over rewriting it from scratch. In workbench,
compare local storage and remote state wiring against `BrowserKeyValueStore`,
`AtomRpc`, `AtomHttpApi`, `AsyncResult`, and `Reactivity`.
## Report Schema
Each scout must return findings in this shape:
```md
## Finding: <short title>
- Priority: P0 | P1 | P2 | No-op
- Impact: 1-5
- Risk: 1-5
- Confidence: 1-5
- TrustGraph evidence: <path:line> and pattern name
- Effect evidence: <import path> and source path
- Current behavior: <what must be preserved>
- Rewrite shape: <Effect-native replacement direction>
- Tests: <specific existing or required tests>
- Blockers: <unknowns, API constraints, compatibility requirements>
```
Scoring:
- P0: large handrolled lifecycle/concurrency/transport surface with a verified
Effect primitive and clear behavior-preserving route.
- P1: real replacement opportunity, but either risk or compatibility needs a
focused design pass.
- P2: cleanup or local modernization; useful but not strategic.
- No-op: current code is already Effect-native, public-boundary Promise code is
intentional, or the Effect primitive does not actually fit.
## Acceptance Checks
For an audit-only pass:
```sh
git status -sb
git diff --check -- ts/EFFECT_NATIVE_REWRITE_PLAYBOOK.md ts/EFFECT_NATIVE_REWRITE_AUDIT.md
```
For any later implementation PR, rerun the relevant package tests and at least:
```sh
cd ts && bun run check
cd ts && bun run build
cd ts && bun run test
```
If gateway, live services, or workbench behavior changes, also run the existing
smoke lanes from `ts/CLASS_EFFECT_GOAL.md`.

View file

@ -0,0 +1,80 @@
# TrustGraph TS Effect-Native Status
## Current State
The TypeScript port on `ts-port-effect-v4` is Effect-first across the production
runtime surfaces completed in Phases 0-6. The law baseline is empty:
`scripts/effect-laws.allowlist.json` has no baseline entries and one documented
host-boundary exemption. The native-class inventory reports zero blocking
production native classes.
The client package's public barrel exports the Effect RPC gateway surface and
shared data models. The legacy `trustgraph-socket.ts` Promise `BaseApi` shim is
kept out of the package barrel and remains only for direct client tests.
Workbench QA uses its own minimal mock API shape.
## Adapted Effect Laws
- Data that crosses wires, persistence, or domain boundaries uses `S.Class`,
`S.Struct`, or tagged schema unions. Service contracts and option bags may
remain interfaces.
- Production code reads configuration through `Config`, not `process.env`.
- JSON encode/decode goes through Schema codecs such as
`S.UnknownFromJsonString`.
- HTTP clients use `effect/unstable/http` with platform layers.
- Effects run at entrypoints through platform `runMain`; libraries return
`Effect`, `Stream`, `Layer`, or service values.
- Deterministic ordering uses `effect/Array` and explicit `Order`s.
- File/path work uses platform services rather than `node:fs` or `node:path`.
- Errors owned by TrustGraph code use tagged errors on the Effect channel.
## Standing Exemptions
- `packages/workbench/src/main.tsx` may throw if `#root` is missing. This is a
browser host boundary and the app cannot render without the mount point.
- `packages/flow/src/agent/mcp-tool/service.ts` may use the
`@modelcontextprotocol/sdk` client behind Effect wrappers. Effect v4 provides
server-side MCP support here, not a replacement client.
- Provider SDK integrations for OpenAI, Azure OpenAI, OpenAI-compatible,
Mistral, and Ollama stay behind `Effect.tryPromise` and
`makeLanguageModelProvider`. Claude already uses `@effect/ai-anthropic`.
- `PubSubBackend` remains a broker adapter contract. Effect `PubSub` is
in-process only and is not a NATS replacement.
- Tests and QA fixtures are excluded from production law checks.
## Gate Commands
Phase 6 validation:
```sh
bun run check
bun run lint
bunx --bun turbo test build --force
git diff --check
```
Final Phase 7 acceptance, all no-cache where applicable:
```sh
bun run check:tsgo
bunx --bun turbo build lint test --force
bun run --cwd packages/mcp test
bun run workbench:qa
cd deploy && docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build
bun run seed && bun run seed:flows && bun run seed:demo
SKIP_LLM=1 bun run test:pipeline
git diff --check
```
Final source audits:
```sh
bun scripts/check-effect-laws.ts
bun scripts/inventory-native-classes.ts
rg -n "fastify|@fastify/websocket|commander|zod" package.json packages scripts bun.lock
rg -n "@modelcontextprotocol/sdk" packages scripts package.json bun.lock
```
Expected result: no law baseline debt, zero blocking native classes, no Fastify,
Commander, Zod, or server-side MCP SDK reintroduction. The MCP SDK client
exemption remains documented above.

View file

@ -75,12 +75,12 @@ export const Field = S.Struct({
});
export type Field = typeof Field.Type;
export const RowSchema = S.Struct({
export const Row = S.Struct({
name: S.String,
description: S.optionalKey(S.String),
fields: S.Array(Field).pipe(S.mutable),
});
export type RowSchema = typeof RowSchema.Type;
export type Row = typeof Row.Type;
export const LlmResult = S.Struct({
text: S.String,

View file

@ -6,8 +6,13 @@ export * from "./models/Triple.js";
export * from "./models/messages.js";
export * from "./models/namespaces.js";
// Export socket client
export * from "./socket/trustgraph-socket.js";
// Export retained compatibility types from the legacy socket shim.
export type {
ConnectionState,
ExplainEvent,
GraphRagOptions,
StreamingMetadata,
} from "./socket/trustgraph-socket.js";
export * from "./socket/effect-rpc-client.js";
export * from "./rpc/contract.js";

View file

@ -7,7 +7,7 @@ import type {
RpcConnectionState,
} from "./effect-rpc-client.js";
import { getDefaultSocketUrl, getRandomValues } from "./websocket-adapter.js";
import { Match, Option, Schema as S } from "effect";
import { Array as A, Match, Option, Order, Schema as S } from "effect";
import * as Predicate from "effect/Predicate";
// Import all message types for different services
@ -1240,9 +1240,7 @@ export function makeFlowsApi(api: BaseApi) {
return this.getConfigAll().then((r) => {
const config = r as { config?: { prompt?: Record<string, unknown> } };
const promptNs = config.config?.prompt ?? {};
return Object.keys(promptNs)
.filter((k) => k !== "system")
.sort()
return A.sort(Object.keys(promptNs).filter((k) => k !== "system"), Order.String)
.map((id) => ({ id, name: id }));
});
},
@ -2204,9 +2202,7 @@ export function makeConfigApi(api: BaseApi) {
return this.getConfigAll().then((r) => {
const config = r as { config?: { prompt?: Record<string, unknown> } };
const promptNs = config.config?.prompt ?? {};
return Object.keys(promptNs)
.filter((k) => k !== "system")
.sort()
return A.sort(Object.keys(promptNs).filter((k) => k !== "system"), Order.String)
.map((id) => ({ id, name: id }));
});
},

View file

@ -5,7 +5,7 @@
*/
import {NodeRuntime} from "@effect/platform-node";
import {Duration, Effect, HashMap, Match, Option, SynchronizedRef} from "effect";
import {Array as A, Duration, Effect, HashMap, Match, Option, Order, SynchronizedRef} from "effect";
import * as Predicate from "effect/Predicate";
import * as S from "effect/Schema";
import type {
@ -122,26 +122,27 @@ const initialState = (): ConfigServiceState => ({
const getHashMapValue = <K, V>(store: HashMap.HashMap<K, V>, key: K): V | undefined =>
Option.getOrUndefined(HashMap.get(store, key));
const compareText = (left: string, right: string): number =>
left.localeCompare(right);
const compareWorkspace = (left: string, right: string): number =>
const workspaceOrder = Order.make<string>((left, right) =>
left === right
? 0
: left === DEFAULT_WORKSPACE
? -1
: right === DEFAULT_WORKSPACE
? 1
: compareText(left, right);
: Order.String(left, right)
);
const orderByKey = <A>(order: Order.Order<string>): Order.Order<readonly [string, A]> =>
Order.mapInput(order, ([key]) => key);
const workspaceEntries = (store: ConfigStore): ReadonlyArray<readonly [string, WorkspaceStore]> =>
HashMap.toEntries(store).sort(([left], [right]) => compareWorkspace(left, right));
A.sort(HashMap.toEntries(store), orderByKey<WorkspaceStore>(workspaceOrder));
const namespaceEntries = (store: WorkspaceStore): ReadonlyArray<readonly [string, NamespaceStore]> =>
HashMap.toEntries(store).sort(([left], [right]) => compareText(left, right));
A.sort(HashMap.toEntries(store), orderByKey<NamespaceStore>(Order.String));
const valueEntries = (store: NamespaceStore): ReadonlyArray<readonly [string, unknown]> =>
HashMap.toEntries(store).sort(([left], [right]) => compareText(left, right));
A.sort(HashMap.toEntries(store), orderByKey<unknown>(Order.String));
const toPersistedWorkspaces = (
store: ConfigStore,

View file

@ -29,7 +29,7 @@ import {
processorLifecycleError,
topics,
} from "@trustgraph/base";
import {Duration, Effect, HashMap, Match, SynchronizedRef} from "effect";
import {Array as A, Duration, Effect, HashMap, Match, Order, SynchronizedRef} from "effect";
import * as O from "effect/Option";
import * as S from "effect/Schema";
import {ensureDirectoryEffect, joinPath, readTextFileEffect, writeTextFileEffect} from "../runtime/effect-files.js";
@ -137,8 +137,11 @@ const cloneKnowledgeCore = (core: KnowledgeCore): KnowledgeCore => ({
})),
});
const sortedEntries = <A>(store: HashMap.HashMap<string, A>): ReadonlyArray<readonly [string, A]> =>
HashMap.toEntries(store).sort(([left], [right]) => left.localeCompare(right));
const sortedEntries = <Value>(store: HashMap.HashMap<string, Value>): ReadonlyArray<readonly [string, Value]> =>
A.sort(
HashMap.toEntries(store),
Order.make<readonly [string, Value]>(([left], [right]) => Order.String(left, right)),
);
const toPersistedSnapshot = (state: KnowledgeCoreServiceState): PersistedKnowledgeSnapshot => {
const kg: Record<string, KnowledgeCore> = {};

View file

@ -37,7 +37,7 @@ import {
import { makeProcessorProgram } from "@trustgraph/base";
import type { Message } from "@trustgraph/base";
import { NodeRuntime } from "@effect/platform-node";
import { Duration, Effect, HashMap, Match, Option, SynchronizedRef } from "effect";
import { Array as A, Duration, Effect, HashMap, Match, Option, Order, SynchronizedRef } from "effect";
import * as S from "effect/Schema";
// ---------- Internal state types ----------
@ -236,8 +236,11 @@ const isStringRecord = (value: unknown): value is Record<string, string> =>
const getHashMapValue = <K, V>(store: HashMap.HashMap<K, V>, key: K): V | undefined =>
Option.getOrUndefined(HashMap.get(store, key));
const sortedEntries = <A>(store: HashMap.HashMap<string, A>): ReadonlyArray<readonly [string, A]> =>
HashMap.toEntries(store).sort(([left], [right]) => left.localeCompare(right));
const sortedEntries = <Value>(store: HashMap.HashMap<string, Value>): ReadonlyArray<readonly [string, Value]> =>
A.sort(
HashMap.toEntries(store),
Order.make<readonly [string, Value]>(([left], [right]) => Order.String(left, right)),
);
const sortedKeys = <A>(store: HashMap.HashMap<string, A>): Array<string> =>
sortedEntries(store).map(([key]) => key);

View file

@ -35,7 +35,7 @@ import {
} from "@trustgraph/base";
import type { Message } from "@trustgraph/base";
import { NodeRuntime } from "@effect/platform-node";
import { Clock, Config, DateTime, Duration, Effect, Match, Option, Random, SynchronizedRef } from "effect";
import { Clock, Config, DateTime, Duration, Effect, Match, Option, Order, Random, SynchronizedRef } from "effect";
import * as A from "effect/Array";
import * as MutableHashMap from "effect/MutableHashMap";
import * as S from "effect/Schema";
@ -479,7 +479,7 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
if (session === undefined) {
return yield* librarianServiceError("get-upload-status", `Upload not found: ${uploadId}`);
}
const receivedChunks = Array.from(MutableHashMap.keys(session.chunks)).sort((a, b) => a - b);
const receivedChunks = A.sort(Array.from(MutableHashMap.keys(session.chunks)), Order.Number);
const receivedSet = new Set(receivedChunks);
const missingChunks = Array.from({ length: session.totalChunks }, (_, i) => i).filter((i) => !receivedSet.has(i));
return {

View file

@ -20,7 +20,7 @@ import type {
TriplesQueryResponse,
} from "@trustgraph/base";
import { Triple, errorMessage } from "@trustgraph/base";
import { Context, Effect, Layer, Match } from "effect";
import { Array as A, Context, Effect, Layer, Match, Order } from "effect";
import * as O from "effect/Option";
import * as S from "effect/Schema";
@ -348,8 +348,10 @@ const scoreEdges = Effect.fn("GraphRagEngine.scoreEdges")(function* (
yield* Effect.log(`[GraphRag] Edge scoring LLM response (first 500 chars): ${llmResp.response.slice(0, 500)}`);
const scored = parseScoredEdges(llmResp.response);
scored.sort((a, b) => b.score - a.score);
const scored = A.sort(
parseScoredEdges(llmResp.response),
Order.make<typeof ScoredEdge.Type>((left, right) => Order.Number(right.score, left.score)),
);
const topN = scored.slice(0, config.edgeLimit);
const result: Triple[] = [];

View file

@ -9,7 +9,7 @@ import type {
import {
makeTrustGraphGatewayClientScoped,
} from "@trustgraph/client";
import {Clock, Config, Context, Effect, Layer} from "effect";
import {Array as A, Clock, Config, Context, Effect, Layer, Order} from "effect";
import * as O from "effect/Option";
import * as Predicate from "effect/Predicate";
import {McpServer, Tool, Toolkit} from "effect/unstable/ai";
@ -1775,9 +1775,7 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
).pipe(
Effect.map((response) => {
const promptNs = asRecord(asRecord(response.config).prompt)
const prompts = Object.keys(promptNs)
.filter((key) => key !== "system")
.sort()
const prompts = A.sort(Object.keys(promptNs).filter((key) => key !== "system"), Order.String)
.map((id) => ({id, name: id}))
return GetPromptsSuccess.make({prompts})
}),

View file

@ -19,8 +19,9 @@ import {
GatewayWorkbenchHttpApi,
TrustGraphRpcs,
} from "@trustgraph/client";
import type { WorkbenchQaApi } from "@/qa/mock-api";
import type { Scope, } from "effect";
import { Cause, Clock, Context, Effect, Layer, Match, Metric, Option, Random, Schema as S, Stream } from "effect";
import { Cause, Clock, Context, Effect, Layer, Match, Metric, Option, Order, Random, Schema as S, Stream } from "effect";
import * as A from "effect/Array";
import * as MutableHashMap from "effect/MutableHashMap";
import * as Predicate from "effect/Predicate";
@ -802,9 +803,9 @@ function ensureNoGatewayResponseError<A>(operation: string, value: A): Effect.Ef
: Effect.fail(WorkbenchPromiseError.make({ cause: value, message: `${operation}: ${message}` }));
}
function qaBaseApi(): import("@trustgraph/client").BaseApi | undefined {
function qaBaseApi(): WorkbenchQaApi | undefined {
if (typeof window === "undefined") return undefined;
return (window as Window & { __TRUSTGRAPH_WORKBENCH_QA_API__?: import("@trustgraph/client").BaseApi }).__TRUSTGRAPH_WORKBENCH_QA_API__;
return (window as Window & { __TRUSTGRAPH_WORKBENCH_QA_API__?: WorkbenchQaApi }).__TRUSTGRAPH_WORKBENCH_QA_API__;
}
function makeWorkbenchGatewayApi(settings: Settings) {
@ -897,9 +898,7 @@ function makeWorkbenchGatewayApi(settings: Settings) {
Effect.map((response) => {
const config = asJsonRecord(response.config);
const promptNs = asJsonRecord(config.prompt);
return Object.keys(promptNs)
.filter((key) => key !== "system")
.sort()
return A.sort(Object.keys(promptNs).filter((key) => key !== "system"), Order.String)
.map((id) => ({ id, name: id }));
}),
),
@ -1826,9 +1825,9 @@ function explainTriplesKey(input: ExplainTriplesInput): string {
const graphs = input.events
.map((event) => event.explainGraph ?? event.explainId ?? "")
.filter((id) => id.length > 0)
.sort()
const sortedGraphs = A.sort(graphs, Order.String)
.join(explainGraphSeparator);
return [input.flowId, input.collection, graphs].join(atomFamilyKeySeparator);
return [input.flowId, input.collection, sortedGraphs].join(atomFamilyKeySeparator);
}
const graphTriplesAtomByKey = Atom.family((key: string) => {

View file

@ -1,5 +1,6 @@
import { lazy, Suspense } from "react";
import { useAtom, useAtomValue } from "@effect/atom-react";
import { Array as A, Order } from "effect";
import { Network, ChevronRight, ChevronDown, Loader2 } from "lucide-react";
import * as Atom from "effect/unstable/reactivity/Atom";
import {
@ -74,7 +75,7 @@ export function ExplainGraph({ explainEvents, collection }: ExplainGraphProps) {
const loading = expanded && resultLoading(result, triples);
const error = resultError(result);
const { data: graphData, typeMap } = triplesToGraph(triples);
const uniqueTypes = Array.from(new Set(Array.from(typeMap.values()).map(localName))).sort();
const uniqueTypes = A.sort(Array.from(new Set(Array.from(typeMap.values()).map(localName))), Order.String);
return (
<div className="mt-2 rounded-md border border-border/50">

View file

@ -1,5 +1,6 @@
import { lazy, Suspense } from "react";
import { useAtom, useAtomRefresh, useAtomValue } from "@effect/atom-react";
import { Array as A, Order } from "effect";
import {
Rotate3d,
Search,
@ -180,7 +181,7 @@ export default function GraphPage() {
const selectedNode = view.selectedNodeId !== null
? data.nodes.find((node) => node.id === view.selectedNodeId)
: undefined;
const uniqueTypes = Array.from(new Set(Array.from(typeMap.values()).map(localName))).sort();
const uniqueTypes = A.sort(Array.from(new Set(Array.from(typeMap.values()).map(localName))), Order.String);
return (
<div className="flex h-full flex-col">

View file

@ -8,7 +8,7 @@ import {
flowIdAtom,
settingsAtom,
} from "@/atoms/workbench";
import type { BaseApi } from "@trustgraph/client";
import type { WorkbenchQaApi } from "@/qa/mock-api";
import { MockWorkbenchFixture, makeMockBaseApi, qaSettingsFromFixture, } from "@/qa/mock-api";
import { Schema as S } from "effect";
@ -21,7 +21,7 @@ export class WorkbenchQaWindowConfig extends S.Class<WorkbenchQaWindowConfig>("W
declare global {
interface Window {
__TRUSTGRAPH_WORKBENCH_QA__?: WorkbenchQaWindowConfig;
__TRUSTGRAPH_WORKBENCH_QA_API__?: BaseApi;
__TRUSTGRAPH_WORKBENCH_QA_API__?: WorkbenchQaApi;
}
}

View file

@ -1,9 +1,26 @@
import type { BaseApi, DocumentMetadata, ProcessingMetadata, StreamingMetadata, Triple } from "@trustgraph/client";
import { makeBaseApiWithRpc, } from "@trustgraph/client";
import { Match, Option, Schema as S } from "effect";
import type { DocumentMetadata, ProcessingMetadata, StreamingMetadata, Triple } from "@trustgraph/client";
import { Array as A, Match, Option, Order, Schema as S } from "effect";
type ConfigValues = Record<string, Record<string, unknown>>;
export interface WorkbenchQaApi {
readonly makeRequest: <RequestType extends object, ResponseType>(
service: string,
request: RequestType,
timeout?: number,
retries?: number,
flow?: string,
) => Promise<ResponseType>;
readonly makeRequestMulti: <RequestType extends object, ResponseType>(
service: string,
request: RequestType,
receiver: (resp: unknown) => boolean,
timeout?: number,
retries?: number,
flow?: string,
) => Promise<ResponseType>;
}
const UnknownRecord = S.Record(S.String, S.Unknown);
const ConfigValuesRecord = S.Record(S.String, UnknownRecord);
@ -358,7 +375,9 @@ function dispatchFlow(state: MockState, request: Record<string, unknown>): unkno
const id = stringValue(request["flow-id"], "default");
return { flow: encodeJson(state.flows.definitions[id] ?? { id, description: "Mock flow" }) };
}
if (operation === "list-blueprints") return { "blueprint-names": Object.keys(state.flows.blueprints).sort() };
if (operation === "list-blueprints") {
return { "blueprint-names": A.sort(Object.keys(state.flows.blueprints), Order.String) };
}
if (operation === "get-blueprint") {
const name = stringValue(request["blueprint-name"], "qa-blueprint");
return { "blueprint-definition": encodeJson(state.flows.blueprints[name] ?? {}) };
@ -563,33 +582,37 @@ function dispatchStream<ResponseType>(
return Promise.resolve({} as ResponseType);
}
export function makeMockBaseApi(fixture: MockWorkbenchFixture = {}): BaseApi {
export function makeMockBaseApi(fixture: MockWorkbenchFixture = {}): WorkbenchQaApi {
const state = createState(fixture);
const token = state.settings.apiKey.length > 0 ? state.settings.apiKey : undefined;
return makeBaseApiWithRpc(state.settings.user, token, state.settings.gatewayUrl, {
dispatch: (input) =>
return {
makeRequest: <RequestType extends object, ResponseType>(
service: string,
request: RequestType,
_timeout?: number,
_retries?: number,
flow?: string,
): Promise<ResponseType> =>
Promise.resolve(
dispatchRequest(
state,
input.service,
input.request,
input.flow,
),
service,
request as Record<string, unknown>,
flow,
) as ResponseType,
),
dispatchStream: (input, receiver) =>
dispatchStream(state, input.service, (message) => {
makeRequestMulti: <RequestType extends object, ResponseType>(
service: string,
_request: RequestType,
receiver: (resp: unknown) => boolean,
): Promise<ResponseType> =>
dispatchStream(state, service, (message) => {
const chunk = message as { response?: unknown; complete?: boolean };
return receiver({
response: chunk.response,
complete: chunk.complete === true,
});
}).then(() => undefined),
subscribe: (listener) => {
listener({ status: token === undefined ? "connected" : "connected" });
return () => {};
},
close: () => Promise.resolve(),
});
}).then(() => ({}) as ResponseType),
};
}
export function qaSettingsFromFixture(fixture: MockWorkbenchFixture = {}) {

View file

@ -1 +1 @@
{"exemptions":[],"baseline":[{"rule":"no-error-throw","path":"packages/workbench/src/main.tsx","count":1},{"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/mcp/src/server-effect.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-schema-suffix","path":"packages/base/src/schema/primitives.ts","count":1}]}
{"exemptions":[{"rule":"no-error-throw","path":"packages/workbench/src/main.tsx","reason":"Host boundary: Workbench cannot render without the #root mount element."}],"baseline":[]}

View file

@ -11,6 +11,7 @@
import { BunRuntime } from "@effect/platform-bun";
import * as BunHttpClient from "@effect/platform-bun/BunHttpClient";
import { DispatchInput, makeEffectRpcClient } from "@trustgraph/client";
import { Config, Effect, Option as O, Schema as S } from "effect";
import { HttpClient, HttpClientRequest } from "effect/unstable/http";
@ -19,17 +20,6 @@ const DEFAULT_LLM_MODEL = "qwen2.5:0.5b";
const DEFAULT_FALKORDB_URL = "redis://localhost:6380";
const DEFAULT_PIPELINE_WAIT = 20;
interface RpcSocket {
close: () => void;
makeRequest: <RequestType extends object, ResponseType>(
service: string,
request: RequestType,
timeout?: number,
retries?: number,
flow?: string,
) => Promise<ResponseType>;
}
interface PipelineConfig {
readonly gatewayUrl: string;
readonly gatewaySecret: string | undefined;
@ -299,33 +289,24 @@ const testTextCompletion = (config: PipelineConfig) => catchTest("Text completio
}));
const testWebSocket = (config: PipelineConfig) => catchTest("Effect RPC WebSocket", Effect.gen(function* () {
let socket: RpcSocket | undefined;
return yield* Effect.gen(function* () {
const { createTrustGraphSocket } = yield* Effect.tryPromise({
try: () => import("../packages/client/src/socket/trustgraph-socket.js"),
catch: (cause) => pipelineError("websocket.import", cause),
});
const gatewayWsUrl = config.gatewayUrl.replace(/^http/, "ws").replace(/\/$/, "");
const response = yield* Effect.scoped(
Effect.gen(function*() {
const client = yield* makeEffectRpcClient(`${gatewayWsUrl}/api/v1/rpc`);
return yield* client.dispatch(
DispatchInput.make({
scope: "global",
service: "config",
request: { operation: "list", keys: [] },
}),
{ timeoutMs: 5_000 },
);
}),
).pipe(Effect.timeout("5 seconds"));
const gatewayWsUrl = config.gatewayUrl.replace(/^http/, "ws").replace(/\/$/, "");
socket = createTrustGraphSocket(
"pipeline",
config.gatewaySecret,
`${gatewayWsUrl}/api/v1/rpc`,
);
const response = yield* Effect.tryPromise({
try: () =>
socket?.makeRequest<Record<string, unknown>, Record<string, unknown>>(
"config",
{ operation: "list", keys: [] },
5000,
) ?? Promise.resolve({}),
catch: (cause) => pipelineError("websocket.request", cause),
}).pipe(Effect.timeout("5 seconds"));
log("websocket/rpc-response", response);
pass("Effect RPC WebSocket round-trip works");
return true;
}).pipe(Effect.ensuring(Effect.sync(() => socket?.close())));
log("websocket/rpc-response", response);
pass("Effect RPC WebSocket round-trip works");
return true;
}));
// ─── Librarian Tests ──────────────────────────────────────────────────