context-witness

Decide between Context Tag witness and capability patterns for dependency injection, understanding coupling trade-offs

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "context-witness" with this command: npx skills add front-depiction/claude-setup/front-depiction-claude-setup-context-witness

Context Witness Pattern

Choose between witness (existence) and capability (behavior) patterns for Context Tags.

Coupling: Hard vs Soft

Some coupling is necessary and good - but move it from hard to soft coupling.

Hard Coupling (Schema)

Field exists in the schema - tightly coupled to domain model:

import { Schema } from "effect"

// ❌ HARD COUPLING - Serial is part of the schema
export const PaymentIntent = Schema.Struct({
  id: Schema.String,
  serial: Schema.String,  // In schema = hard coupled
  amount: Schema.BigInt
})

// Every PaymentIntent MUST have a serial
// Serialization/validation requires serial
// Cannot create without providing serial
// Schema change needed to remove/change serial

Soft Coupling (Witness)

Field removed from schema, only injected in code:

import { Schema, Context, Effect, Logger } from "effect"

declare const generateId: () => string

// ✅ SOFT COUPLING - Serial not in schema
export const PaymentIntent = Schema.Struct({
  id: Schema.String,
  amount: Schema.BigInt
  // No serial field!
})

// Serial is a witness - required but injected via Context
class Serial extends Context.Tag("Serial")<Serial, string>() {}

const createPaymentIntent = (amount: bigint) =>
  Effect.gen(function* () {
    const serial = yield* Serial  // Injected from context

    // Use serial in business logic, logging, etc.
    // but it's not part of the persisted data
    yield* Logger.info(`Creating payment intent ${serial}`)

    return PaymentIntent.make({ id: generateId(), amount })
  })

// Type: Effect<PaymentIntent, never, Serial>

Key insight: schema (hard coupling) => witness (soft coupling)

By removing the field from the schema and injecting it only where needed, you:

  • Keep domain models minimal
  • Avoid unnecessary persistence
  • Easy to test (provide test serial)
  • Easy to remove/change (just change injection)
  • Explicit dependencies in type signature

When to use witnesses:

  • Correlation IDs (for tracing, not persistence)
  • Request IDs (for logging, not data)
  • Transaction contexts (for coordination, not storage)
  • Tenant/Region markers (for routing, not schema)

Witness: Existence Only

Use when you only need to know something exists in the environment:

import { Schema, Context, Effect } from "effect"

declare const PaymentIntent: Schema.Struct<{
  id: typeof Schema.String
  serial: typeof Schema.String
  amount: typeof Schema.BigInt
}>
declare const other: any

// Witness - a serial number exists
export class Serial extends Context.Tag("Serial")<Serial, string>() {}

const createPaymentIntent = Effect.gen(function* () {
  const serial = yield* Serial  // Pull from environment
  return PaymentIntent.make({ serial, ...other })
})

// Type: Effect<PaymentIntent, never, Serial>

Capability: Behavior

Use when you need operations:

import { Schema, Context, Effect } from "effect"

declare const PaymentIntent: Schema.Struct<{
  id: typeof Schema.String
  serial: typeof Schema.String
  amount: typeof Schema.BigInt
}>
declare const other: any

// Capability - can generate/validate
export class SerialService extends Context.Tag("SerialService")<
  SerialService,
  {
    readonly next: () => string
    readonly validate: (s: string) => boolean
  }
>() {}

const createPaymentIntent = Effect.gen(function* () {
  const svc = yield* SerialService
  const serial = svc.next()  // Behavior
  return PaymentIntent.make({ serial, ...other })
})

// Type: Effect<PaymentIntent, never, SerialService>

Decision Framework

NeedPattern
Just presence/valueWitness
Operations/generationCapability
Precondition markerWitness
Side effectsCapability
Multiple implementationsCapability
Mocking behaviorCapability
Correlation IDWitness
Transaction contextWitness
LoggerCapability
DatabaseCapability

When to Use Witness

Good fits:

  • Request ID - must exist for tracing
  • Transaction context - must be established
  • Tenant/Region - required for data boundary
  • Pre-validated tokens - already verified

When to Use Capability

Good fits:

  • Serial generation - create/validate operations
  • Clock - now() operation
  • Logger - structured logging methods
  • Database - query/transact operations
  • HTTP clients - fetch/post operations

Testing Implications

Witnesses are trivial to provide:

import { Effect } from "effect"

declare const myProgram: Effect.Effect<unknown, never, Serial>
declare class Serial extends Context.Tag("Serial")<Serial, string>() {}

const test = myProgram.pipe(
  Effect.provideService(Serial, "test-serial-123")
)

Capabilities need implementation:

import { Effect } from "effect"

declare const myProgram: Effect.Effect<unknown, never, SerialService>
declare class SerialService extends Context.Tag("SerialService")<
  SerialService,
  {
    readonly next: () => string
    readonly validate: (s: string) => boolean
  }
>() {}

const test = myProgram.pipe(
  Effect.provideService(SerialService, {
    next: () => "test-serial-123",
    validate: () => true
  })
)

Coupling Strategy

Rule of thumb: Remove non-essential fields from schema, inject via witness instead.

Ask yourself: Does this need to be persisted/serialized?

  • No → Remove from schema, inject via witness
  • Yes → Keep in schema
import { Schema, Context, Effect, Logger, Clock } from "effect"

declare const LineItem: Schema.Schema<any>
declare const generateId: () => string
declare const calculateTotal: (items: Array<any>) => bigint

// ✅ Domain model - only persisted data
export const Order = Schema.Struct({
  id: Schema.String,
  items: Schema.Array(LineItem),
  total: Schema.BigInt
  // No correlationId - not persisted!
  // No timestamp - derived from system!
})

// Witnesses for runtime context
class CorrelationId extends Context.Tag("CorrelationId")<CorrelationId, string>() {}
class RequestId extends Context.Tag("RequestId")<RequestId, string>() {}

// Use in code, not in data
const createOrder = (items: Array<Schema.Schema.Type<typeof LineItem>>) =>
  Effect.gen(function* () {
    const correlationId = yield* CorrelationId  // For tracing
    const requestId = yield* RequestId          // For logging
    const clock = yield* Clock                  // For timestamp

    yield* Logger.info({
      message: "Creating order",
      correlationId,    // Used for tracing
      requestId,        // Used for logging
      timestamp: Clock.currentTimeMillis(clock)
    })

    // Data only contains what's persisted
    return Order.make({
      id: generateId(),
      items,
      total: calculateTotal(items)
    })
  })

// Type: Effect<Order, never, CorrelationId | RequestId | Clock>

Benefits:

  • Minimal schemas (only persisted data)
  • Context values available when needed
  • Easy to test with different context
  • Can add/remove context without schema changes
  • Explicit dependencies in type signatures

Choose witness for simplicity, capability for flexibility.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

command-executor

No summary provided by upstream source.

Repository SourceNeeds Review
General

react-composition

No summary provided by upstream source.

Repository SourceNeeds Review
General

wide-events

No summary provided by upstream source.

Repository SourceNeeds Review
General

effect-ai-streaming

No summary provided by upstream source.

Repository SourceNeeds Review