Skip to content

@manifesto-ai/effect-utils — Foundational Design Rationale (FDR)

Version: 1.0
Status: Draft
Purpose: Document the "why" behind every major design decision for effect-utils


Table of Contents

  1. Purpose of This Document
  2. FDR-EU-001: Separate Package from Host
  3. FDR-EU-002: Function Composition over Configuration
  4. FDR-EU-003: Combinators as Building Blocks
  5. FDR-EU-004: Primitives Only — No Domain Adapters
  6. FDR-EU-005: Schema-Driven Handler Factory
  7. FDR-EU-006: Settled Type for Partial Failures
  8. FDR-EU-007: Patch Transform Helpers
  9. FDR-EU-008: No Async Iterator / Stream Abstractions
  10. Summary: The effect-utils Identity

1. Purpose of This Document

This document records the foundational design decisions of @manifesto-ai/effect-utils.

effect-utils exists because:

  1. Host Contract defines "what" handlers must do — but not "how" to build them
  2. Common patterns emerge — timeout, retry, parallel, fallback
  3. Boilerplate accumulates — every handler repeats similar logic
  4. Type safety mattersRecord<string, unknown> is not good enough

For each decision, we document:

SectionContent
DecisionWhat we decided
ContextWhy this decision was needed
AlternativesWhat other options existed
RationaleWhy we chose this option
ConsequencesWhat this enables and constrains

FDR-EU-001: Separate Package from Host

Decision

effect-utils is a separate package from @manifesto-ai/host.

@manifesto-ai/effect-utils   ← New package

        ▼ (types only)
@manifesto-ai/core           ← Patch, Snapshot types

@manifesto-ai/host

        ├──▶ @manifesto-ai/core
        └──▶ @manifesto-ai/effect-utils (optional peer)

Context

Effect Handler utilities could live in several places:

  1. Inside @manifesto-ai/host
  2. Inside @manifesto-ai/core
  3. As a separate package

Host already has responsibilities:

  • Compute loop orchestration
  • Effect execution coordination
  • Requirement fulfillment
  • Snapshot persistence

Adding utilities would bloat Host and create mixed concerns.

Alternatives Considered

AlternativeDescriptionWhy Rejected
Utilities in HostExport helpers from host packageBloats Host, forces unnecessary dependencies
Utilities in CoreExport helpers from core packageViolates Core purity (utilities are for IO)
No utilitiesLet developers write their ownBoilerplate, inconsistent patterns

Rationale

Separation enables:

BenefitDescription
Independent versioningUtils can iterate without Host changes
Optional adoptionExisting Host users aren't forced to update
TestabilityHandlers can be unit tested without Host
Tree-shakingApps only bundle what they use

Mirrors Builder pattern:

Builder : Core = effect-utils : Host

Builder provides DX for defining domains (produces Schema)
effect-utils provides DX for implementing handlers (produces Patch[])

Consequences

EnablesConstrains
Focused, small packagesOne more package to manage
Independent testingNeed to coordinate releases
Optional adoptionDocumentation must explain relationship
Clear ownership

Canonical Statement

effect-utils is to Host what Builder is to Core: a DX layer that doesn't execute.


FDR-EU-002: Function Composition over Configuration

Decision

effect-utils uses function composition pattern, not configuration objects.

typescript
// ✅ Function composition
const handler = withRetry(
  withTimeout(
    fetchData,
    5000
  ),
  { maxRetries: 3 }
);

// ❌ NOT configuration object
const handler = createHandler({
  fetch: fetchData,
  timeout: 5000,
  retry: { max: 3 },
  fallback: null,
  // ... 20 more options
});

Context

Two paradigms exist for building complex behavior:

Configuration-based:

typescript
createFetcher({
  baseUrl: 'https://api.example.com',
  timeout: 5000,
  retry: { max: 3, backoff: 'exponential' },
  auth: { type: 'bearer', token: '...' },
  cache: { ttl: 60000 },
  transform: (data) => data.results,
  onError: (err) => console.error(err),
  // Options keep growing...
});

Composition-based:

typescript
pipe(
  fetchJson,
  withAuth(bearerToken),
  withTimeout(5000),
  withRetry(3),
  withCache(60000),
  mapResult(data => data.results)
);

Alternatives Considered

AlternativeDescriptionWhy Rejected
Config objectsSingle object with all optionsOption explosion, hard to type, hard to extend
Builder patternnew Handler().timeout(5000).retry(3)Mutable, method chaining context hard for LLMs
Decorator pattern@timeout(5000) @retry(3) class HandlerTypeScript decorators are unstable, class-based

Rationale

For Developers:

AspectConfigurationComposition
Learning curveMemorize all optionsLearn small functions
CustomizationFork or extend configCompose new functions
TestingMock entire configTest each function
Type inferenceComplex conditional typesSimple generics

For LLMs:

AspectConfigurationComposition
ParsingMust understand all optionsEach function is independent
GenerationOption combinations explodeLinear composition
ValidationSchema validation complexType check each step
Explanation"What does this config do?""A wraps B wraps C"
typescript
// LLM can understand this step by step:
withRetry(           // 3. If it fails, retry up to 3 times
  withTimeout(       // 2. With a 5 second timeout
    fetchAIS,        // 1. Fetch AIS data
    5000
  ),
  { maxRetries: 3 }
)

Consequences

EnablesConstrains
Small, focused functionsNo single "configure everything" API
Easy to extendComposition order matters
LLM-friendlySlightly more verbose
Type inference works naturally

Canonical Statement

Small functions that compose beat large configs that configure.


FDR-EU-003: Combinators as Building Blocks

Decision

effect-utils provides a small set of stateless combinators that handle common execution patterns.

Core Combinators (v1.0):

CombinatorPurposeSignature
withTimeoutTime-bound execution(fn, ms) → fn'
withRetryRetry on failure(fn, options) → fn'
withFallbackDefault on failure(fn, fallback) → fn'
parallelConcurrent execution(fns) → fn
raceFirst success wins(fns) → fn
sequentialOrdered execution(fns) → fn

Explicitly Excluded (stateful, Host policy domain):

PatternWhy Excluded
circuitBreakerRequires cross-request state
rateLimitRequires cross-request state
cacheRequires cross-request state
bulkheadRequires cross-request state

Context

Every Effect Handler eventually needs:

  • Timeout handling (external APIs can hang)
  • Retry logic (transient failures happen)
  • Fallback values (graceful degradation)
  • Parallel execution (multiple independent calls)

Without utilities, every handler reimplements these:

typescript
// Without combinators — repeated in every handler
async function myHandler(params) {
  let attempts = 0;
  while (attempts < 3) {
    try {
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), 5000);
      const result = await fetch(url, { signal: controller.signal });
      clearTimeout(timeout);
      return [{ op: 'set', path: 'data', value: result }];
    } catch (e) {
      attempts++;
      if (attempts >= 3) return [{ op: 'set', path: 'error', value: e.message }];
      await sleep(attempts * 1000);
    }
  }
}

Alternatives Considered

AlternativeDescriptionWhy Rejected
Full RxJS integrationObservable-based combinatorsHeavy dependency, learning curve
Effect-TS styleAlgebraic effectsParadigm shift too large
Minimal (just types)Only provide types, no runtimeDoesn't reduce boilerplate
Include circuit breakerStateful resilienceViolates stateless principle (see below)

Rationale

Goldilocks principle: Enough to eliminate boilerplate, not so much that it becomes a framework.

typescript
// With combinators — declarative, composable
const fetchWithResilience = withRetry(
  withTimeout(fetch, 5000),
  { maxRetries: 3, backoff: 'exponential' }
);

const handler = async (params) => {
  const result = await fetchWithResilience(params.url);
  return [{ op: 'set', path: 'data', value: result }];
};

Combinator selection criteria:

CriterionIncludedExcluded
Needed in >50% of handlers✅ timeout, retry
Stateless (no cross-request memory)✅ all v1.0 combinators❌ circuit breaker
Framework-agnostic✅ pure functions❌ React-specific
Simple signaturefn → fn❌ complex type gymnastics

Why Circuit Breaker is Host Policy, Not effect-utils

Host Contract §14 explicitly grants policy freedom:

Host MAY: Retry, Circuit break, Timeout, Parallelize... These are policy decisions, not part of the Contract.

Circuit breaker requires:

  1. State — failure count, last failure time, circuit state
  2. Cross-request memory — "this endpoint failed 5 times in last minute"
  3. Policy decisions — when to open, when to half-open, when to close

This belongs in Host (or @manifesto-ai/host-utils if needed), not effect-utils.

"Stateless circuit breaker" is a contradiction:

typescript
// ❌ "Stateless circuit breaker" degrades to just retry + timeout
const pseudoCircuitBreaker = withRetry(
  withTimeout(fn, 1000),
  { maxRetries: 0 }  // fail fast
);
// This is NOT a circuit breaker — it has no memory across requests

Correct pattern: circuit open → immediate failure patch

typescript
// Host-level circuit breaker (outside effect-utils)
class HostCircuitBreaker {
  private state: Map<string, CircuitState> = new Map();
  
  async executeEffect(req: Requirement): Promise<Patch[]> {
    const circuit = this.state.get(req.type);
    
    if (circuit?.isOpen) {
      // Don't skip — return failure patches per Host Contract
      return [
        toErrorPatch('system.lastError', {
          code: 'CIRCUIT_OPEN',
          message: `Circuit breaker open for ${req.type}`
        })
      ];
    }
    
    // Execute and update circuit state...
  }
}

Consequences

EnablesConstrains
Consistent resilience patternsNo stateful patterns in v1.0
Composable building blocksCircuit breaker needs Host-level solution
Predictable behaviorNo magic, explicit composition
Small bundle size
Clear boundary with Host

Canonical Statement

Combinators transform functions. They are stateless. Stateful resilience patterns belong in Host.


FDR-EU-004: Primitives Only — No Domain Adapters

Decision

effect-utils provides only primitives. It does NOT provide:

  • HTTP clients
  • Database adapters
  • Message queue connectors
  • Domain-specific protocols (AIS, EDI, SOAP, etc.)

Context

The temptation is strong:

typescript
// "Wouldn't it be nice if..."
import { httpEffect, wsEffect, dbEffect } from '@manifesto-ai/effect-utils';

const handler = httpEffect({
  url: '/api/data',
  method: 'GET',
  timeout: 5000
});

But this path leads to:

  • Maintaining adapters for every protocol
  • Version conflicts with native SDKs
  • Always being behind latest API changes
  • Framework bloat

Alternatives Considered

AlternativeDescriptionWhy Rejected
Include HTTP adapterBuilt-in fetch wrapperEveryone has preferences (axios, ky, got)
Include DB adaptersPrisma, Drizzle, etc.Version conflicts, massive scope
Adapter plugin systemregisterAdapter('http', ...)Plugin systems add complexity

Rationale

Manifesto provides the "what", developers provide the "how".

effect-utils provides:
├── withTimeout()     ← Generic timing
├── withRetry()       ← Generic resilience  
├── parallel()        ← Generic concurrency
└── toPatch()         ← Generic transformation

Developers provide:
├── aisClient         ← Their AIS SDK
├── maerskApi         ← Their Maersk integration
├── unipassClient     ← Their customs API
└── legacySoapClient  ← Their legacy adapter

Comparison with ecosystem:

LibraryProvides PrimitivesProvides Adapters
Redux Sagacall, put
React Query✅ caching, retry❌ (you provide fetcher)
effect-utils✅ combinators

Consequences

EnablesConstrains
Zero opinions on HTTP librariesDevelopers choose their stack
No version conflictsMore integration code
Small package sizeNo "batteries included"
Focus on core value

Canonical Statement

effect-utils makes building handlers easier. It doesn't build them for you.


FDR-EU-005: Schema-Driven Handler Factory

Decision

effect-utils provides defineEffectSchema and createHandler for type-safe handler creation.

Zod is a required dependency (dependencies, not peerDependencies).

typescript
// 1. Define schema
const myEffectSchema = defineEffectSchema({
  type: 'api.myEffect',
  input: z.object({
    userId: z.string(),
    limit: z.number().default(10)
  }),
  output: z.object({
    items: z.array(ItemSchema),
    total: z.number()
  }),
  outputPath: 'items'
});

// 2. Create handler with full type inference
const myHandler = createHandler(myEffectSchema, async (input, snapshot) => {
  // input is typed: { userId: string, limit: number }
  // snapshot is Readonly<Snapshot> — read-only context per Host Contract
  const result = await fetchItems(input.userId, input.limit);
  // return is validated against output schema
  return { items: result.items, total: result.total };
});

Context

Host Contract defines handler signature as:

typescript
type EffectHandler = (
  type: string,
  params: Record<string, unknown>  // ← No type safety
) => Promise<Patch[]>;

This means:

  • params is untyped
  • Return type is just Patch[]
  • No validation of effect type matching
  • Runtime errors instead of compile errors

Alternatives Considered

AlternativeDescriptionWhy Rejected
No schema, just typesas MyParams castingNo runtime validation
JSON SchemaStandard schema formatWorse TypeScript integration than Zod
Manual validationDeveloper validates in handlerBoilerplate, inconsistent
Zod as peer optionalLet users choose schema libType-level exposure requires Zod anyway; DX/docs/LLM generation all diverge

Rationale

Why Zod is required (not optional peer):

  1. Builder already adopted Zod-first — Consistency across Manifesto packages
  2. Type-level exposure — Once z.ZodTypeAny appears in types, Zod must be installed anyway
  3. DX clarity — Optional peer creates "installed but not imported" runtime errors
  4. LLM generation — Single schema library means no branching in generated code

Zod provides both runtime validation and type inference:

typescript
const schema = z.object({ userId: z.string() });

// Runtime: validates actual data
schema.parse(untrustedInput);

// Compile time: infers TypeScript type
type Input = z.infer<typeof schema>;  // { userId: string }

Schema bridges MEL and TypeScript:

MEL Effect Declaration:
effect api.myEffect({ userId: customerId, into: items })

Effect Schema (effect-utils):
defineEffectSchema({ type: 'api.myEffect', input: z.object({...}) })

Handler Implementation:
createHandler(schema, async (input) => { /* typed! */ })

Future Alternative (if needed):

If demand arises for other schema libraries (Valibot, Yup), provide via sub-entrypoint:

typescript
// Default (Zod)
import { defineEffectSchema } from '@manifesto-ai/effect-utils';

// Future: alternative schema libs via sub-entrypoints
import { defineEffectSchema } from '@manifesto-ai/effect-utils/valibot';

This keeps main entrypoint clean while allowing extension.

Handler Contract Alignment

createHandler aligns with Host Contract §7.5:

Host Contract RulecreateHandler Behavior
Handler MAY receive snapshot as read-only contextsnapshot: Readonly<Snapshot> parameter
Handler MUST NOT implement domain logicDeveloper responsibility — documented, not enforced
Handler MUST return Patch[]Auto-transforms output to patches via outputPath
Handler MUST NOT throwWraps implementation in try/catch, converts to error patches

Critical: "No domain logic in handlers" is a MUST NOT from Host Contract.

typescript
// ❌ WRONG: Domain logic in handler
createHandler(schema, async (input, snapshot) => {
  if (input.amount > 1000) {  // Business rule!
    return { requiresApproval: true };
  }
  // ...
});

// ✅ CORRECT: Pure IO, no decisions
createHandler(schema, async (input, snapshot) => {
  const result = await api.fetchData(input.id);
  return result;  // Just return data, let Flow decide
});

Consequences

EnablesConstrains
Type-safe handler developmentZod is required dependency
Runtime input validationSchema must match MEL effect
Output path auto-patching
Self-documenting effects
Consistent with Builder

Canonical Statement

Schema is the contract between MEL declaration and TypeScript implementation. Zod is the language of that contract.


FDR-EU-006: Settled Type for Partial Failures

Decision

parallel() and race() return Settled results, not throwing on partial failure.

typescript
type Settled<T> = 
  | { status: 'fulfilled'; value: T }
  | { status: 'rejected'; reason: Error };

// parallel returns all results, even if some failed
const results = await parallel({
  ais: fetchAIS,
  tos: fetchTOS,
  weather: fetchWeather
})();

// results: {
//   ais: { status: 'fulfilled', value: AisData },
//   tos: { status: 'rejected', reason: Error },
//   weather: { status: 'fulfilled', value: WeatherData }
// }

Context

Standard Promise APIs have different behaviors:

typescript
// Promise.all — fails fast, loses successful results
await Promise.all([fetchA(), fetchB(), fetchC()]);
// If fetchB fails, you don't get fetchA's result

// Promise.allSettled — keeps all results
await Promise.allSettled([fetchA(), fetchB(), fetchC()]);
// Returns all results with status

For Effect Handlers, partial success is common and valuable:

  • API A succeeded, API B failed → still useful
  • 3/5 carriers responded → show partial results

Alternatives Considered

AlternativeDescriptionWhy Rejected
Throw on any failureUse Promise.all behaviorLoses successful results
**Return `Tnull`**Null for failures
Return Result<T, E>Custom Result typeAnother type to learn

Rationale

Settled mirrors Promise.allSettled — already known to JS developers.

typescript
// Standard JavaScript
const results = await Promise.allSettled([p1, p2, p3]);
results[0].status === 'fulfilled' ? results[0].value : results[0].reason;

// effect-utils parallel — same pattern
const results = await parallel({ a: p1, b: p2, c: p3 })();
results.a.status === 'fulfilled' ? results.a.value : results.a.reason;

Enables graceful degradation:

typescript
const results = await parallel({
  ais: fetchAIS,
  tos: fetchTOS,
  weather: fetchWeather
})();

return [
  toPatch('signals.ais', results.ais.status === 'fulfilled' ? results.ais.value : null),
  toPatch('signals.tos', results.tos.status === 'fulfilled' ? results.tos.value : null),
  toPatch('signals.weather', results.weather.status === 'fulfilled' ? results.weather.value : null),
  toPatch('signals.errors', collectRejected(results)),
];

Consequences

EnablesConstrains
Partial success handlingMust check status on each result
Error information preservedSlightly more verbose
Consistent with JS standards
Explicit failure handling

Canonical Statement

Partial failure is not total failure. Keep what succeeded.


FDR-EU-007: Patch Transform Helpers

Decision

effect-utils provides helpers to transform results into Patch[].

typescript
// Single patch
toPatch('user', userData);
// → { op: 'set', path: 'user', value: userData }

// Multiple patches
toPatches({
  'user': userData,
  'loadedAt': context.requirement.createdAt
});
// → [{ op: 'set', path: 'user', value: userData },
//    { op: 'set', path: 'loadedAt', value: 1234567890 }]

// Error patch
toErrorPatch('error', new Error('Failed'));
// → { op: 'set', path: 'error', value: { code: 'Error', message: 'Failed' } }

// Collect errors from Settled results
collectErrors(settledResults, 'signals.errors');
// → [{ op: 'set', path: 'signals.errors', value: { ais: {...}, tos: {...} } }]

Context

Every Effect Handler must return Patch[]. Without helpers:

typescript
async function handler(params, context) {
  const data = await fetchData();
  
  // Manual patch construction — verbose, error-prone
  return [
    { op: 'set', path: 'result', value: data },
    { op: 'set', path: 'loadedAt', value: context.requirement.createdAt },
    { op: 'set', path: 'status', value: 'ready' },
  ];
}

With helpers:

typescript
async function handler(params, context) {
  const data = await fetchData();
  
  return toPatches({
    'result': data,
    'loadedAt': context.requirement.createdAt,
    'status': 'ready',
  });
}

Alternatives Considered

AlternativeDescriptionWhy Rejected
No helpersManual patch constructionBoilerplate, typos in 'op' field
Patch builder classnew PatchBuilder().set(...).merge(...)Over-engineering
Auto-patchingDetect changes automaticallyMagic, hard to debug

Rationale

Helpers reduce ceremony without hiding intent:

typescript
// Before: What is 'set'? What is 'op'? Easy to typo.
{ op: 'set', path: 'x', value: y }

// After: Clear intent, less syntax
toPatch('x', y)

Type safety:

typescript
// toPatch enforces correct structure
function toPatch(path: string, value: unknown): Patch {
  return { op: 'set', path, value };
}

// Can't accidentally create invalid patch
toPatch('x', y);  // Always valid
{ op: 'ste', path: 'x', value: y };  // Typo goes unnoticed

Consequences

EnablesConstrains
Less boilerplateStill manual path strings
Fewer typosPaths not validated at compile time
Consistent error formatting
Readable handler code

Canonical Statement

Helpers reduce syntax, not power. You can still construct patches manually.


FDR-EU-008: No Async Iterator / Stream Abstractions

Decision

effect-utils does NOT provide async iterator, observable, or stream abstractions.

typescript
// ❌ NOT provided
import { stream, observe, iterate } from '@manifesto-ai/effect-utils';

// ❌ NOT provided
effect.stream('ws://quotes', {
  onMessage: (msg) => toPatch('quotes', msg),
  debounce: 500
});

Context

Manifesto's event flow for streaming data:

External Stream (WS/SSE/etc.)


┌───────────────────────────────────────┐
│  Bridge / Ingress Layer               │  ← Streaming belongs HERE
│  ┌─────────────────────────────────┐  │
│  │ SourceEvent Creation            │  │
│  │ - Debouncing / Buffering        │  │
│  │ - Batching                      │  │
│  └─────────────────────────────────┘  │
│              │                        │
│              ▼                        │
│  ┌─────────────────────────────────┐  │
│  │ Projection                      │  │
│  │ SourceEvent → IntentBody        │  │
│  └─────────────────────────────────┘  │
│              │                        │
│              ▼                        │
│  ┌─────────────────────────────────┐  │
│  │ Issuer                          │  │
│  │ IntentBody → IntentInstance     │  │
│  └─────────────────────────────────┘  │
└───────────────────────────────────────┘

        ▼ dispatch(Intent)
┌───────────────────────────────────────┐
│  World Protocol                       │
│  - Proposal, Authority, Decision      │
└───────────────────────────────────────┘

        ▼ approved Intent
┌───────────────────────────────────────┐
│  Host                                 │  ← Request/Response only
│  - compute() loop                     │
│  - Effect execution                   │
│  - Patch application                  │
└───────────────────────────────────────┘

Key insight: Host handles request/response effect fulfillment. Streaming is an ingress concern handled before Intent reaches Host.

Alternatives Considered

AlternativeDescriptionWhy Rejected
RxJS integrationObservable-based effectsHeavy, paradigm shift
AsyncIterator helpersfor await patternsStill push-based, ingress concern
Stream subscriptionBuilt-in WS/SSE supportDomain-specific, wrong layer

Rationale

effect-utils handles request/response. Bridge/Ingress handles push/stream.

effect-utils scope (request/response):
├── Timeout, retry, parallel     ✅
├── Transform to Patch[]         ✅
└── Stream subscription          ❌ (Ingress concern)

Bridge/Ingress scope (event flow):
├── WebSocket/SSE management     ✅
├── Debouncing/buffering         ✅
├── SourceEvent creation         ✅
├── Projection to Intent         ✅
└── Intent issuance              ✅

Host scope (execution):
├── Compute loop                 ✅
├── Effect fulfillment           ✅
├── Patch application            ✅
└── Snapshot persistence         ✅

Intent & Projection Spec alignment:

The Intent & Projection Spec defines SourceEvent kinds:

  • ui — User interface events
  • api — External API calls
  • agent — AI agent actions
  • system — System-generated events

Streaming data (WebSocket quotes, SSE updates) enters as SourceEvents at the Bridge layer, gets projected to Intents, then dispatched to World/Host.

Correct streaming pattern in Manifesto:

typescript
// Bridge/Application layer — NOT effect-utils
class QuoteStreamIngress {
  private ws: WebSocket;
  private buffer: Quote[] = [];
  
  constructor(
    private bridge: Bridge,
    private options: { debounceMs: number; batchSize: number }
  ) {
    this.ws = new WebSocket('wss://quotes');
    this.ws.onmessage = (e) => this.handleMessage(JSON.parse(e.data));
  }
  
  private handleMessage(quote: Quote) {
    this.buffer.push(quote);
    this.scheduleFlush();
  }
  
  private scheduleFlush = debounce(() => {
    if (this.buffer.length === 0) return;
    
    // Create SourceEvent
    const sourceEvent = createAPISourceEvent('quotes-batch', {
      quotes: this.buffer
    });
    
    // Dispatch through Bridge (→ Projection → Intent → World → Host)
    this.bridge.dispatchEvent(sourceEvent);
    this.buffer = [];
  }, this.options.debounceMs);
}

// Effect handler (effect-utils territory) — just handles the batch
const quotesUpdateHandler = createHandler(quotesUpdateSchema, async (input, context) => {
  // input.quotes is already batched by ingress layer
  return {
    latest: input.quotes,
    updatedAt: context.requirement.createdAt
  };
});

Where Should Stream Utilities Live?

OptionRecommendation
In effect-utils❌ Wrong layer
In Host❌ Host is request/response
In Bridge⚠️ Maybe, if common patterns emerge
In app code✅ Default — domain-specific buffering/debouncing
In @manifesto-ai/bridge-utils✅ Future — if common ingress primitives needed

v1.0 Recommendation: Leave to application code. If patterns repeat across projects, extract to @manifesto-ai/bridge-utils or @manifesto-ai/source-utils.

Consequences

EnablesConstrains
Clear layer boundariesNo streaming helpers in effect-utils
Correct architectural alignmentStreaming patterns documented elsewhere
effect-utils stays stateless
Host stays request/response

Canonical Statement

Streams are ingress concerns (Bridge/Application). effect-utils handles request/response effect fulfillment.


Summary: The effect-utils Identity

What effect-utils IS

AspectDescription
DX layerMakes building Effect Handlers easier
Combinator librarySmall functions that compose
Type-safeZod schemas for input/output
StatelessNo internal state, pure transforms
FocusedOnly execution patterns, not IO adapters

What effect-utils IS NOT

AspectWhy Not
HTTP clientUse your preferred library
Database adapterUse your preferred ORM
Stream/Observable libraryHost's responsibility
Full resilience frameworkJust the primitives
Required dependencyHost works without it

Package Boundary

typescript
// @manifesto-ai/effect-utils exports:

// Combinators
export { withTimeout, withRetry, withFallback } from './combinators';
export { parallel, race, sequential } from './combinators';

// Transforms
export { toPatch, toPatches, toErrorPatch, collectErrors } from './transforms';

// Schema
export { defineEffectSchema, createHandler } from './schema';

// Types
export type { Settled, EffectSchema, CombinatorOptions } from './types';

Canonical Statements Summary

FDRStatement
EU-001effect-utils is to Host what Builder is to Core: a DX layer that doesn't execute.
EU-002Small functions that compose beat large configs that configure.
EU-003Combinators transform functions. They are stateless. Stateful resilience patterns belong in Host.
EU-004effect-utils makes building handlers easier. It doesn't build them for you.
EU-005Schema is the contract between MEL declaration and TypeScript implementation. Zod is the language of that contract.
EU-006Partial failure is not total failure. Keep what succeeded.
EU-007Helpers reduce syntax, not power. You can still construct patches manually.
EU-008Streams are ingress concerns (Bridge/Application). effect-utils handles request/response effect fulfillment.

Dependency Direction

@manifesto-ai/effect-utils

        ▼ (types only)
@manifesto-ai/core              ← Patch, Snapshot types
        
        ▲ (optional peer)

@manifesto-ai/host              ← Uses effect-utils for handler DX

Cross-Reference

Related SpecRelationship
Host ContractDefines EffectHandler signature that effect-utils helps implement
MEL SpecDefines effect declarations that effect-utils helps fulfill
Schema SpecDefines Patch structure that effect-utils produces
Builder SpecSimilar DX philosophy, different layer

End of @manifesto-ai/effect-utils FDR v1.0