@systemfsoftware/effect-cell-types 8.0.0 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @systemfsoftware/effect-cell-types
2
2
 
3
+ ## 8.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Build a cell as a typed continuation chain: `Sandwich.read` takes the read effect and each step exposes only the phase that may follow, so a misordered chain fails to compile with a missing-method error naming the lawful next steps. Pure fillings (`decode`, `encode`) accept only `Sandwich.pure` thunks, and every finished cell carries a recorded `phases` tuple naming the steps it runs. The record-spec authoring surface is retired: build chains with `Sandwich.read` instead.
8
+
9
+ ## 8.1.0
10
+
11
+ ### Minor Changes
12
+
13
+ - `collectAll` now declares an error channel of `never`: the composed cell cannot fail, because every per-item refusal is delivered to the fold as a `Result`. Composed cells no longer carry the item error channel in their type.
14
+
15
+ `Workflow.andThen` now composes two total workflows (components whose error channel is `never`) into a total workflow. Error-carrying chains are unchanged.
16
+
17
+ `Workflow.make` and `Workflow.total` now refuse decision unions whose variants declare the family brand as a narrow unique-symbol type instead of the documented class-field idiom. Declare the brand as a `readonly [T] = T` field on each `S.TaggedClass` variant: the field initializer widens the slot to the general `symbol` type, and the predicate keys on that widened slot. A `typeof`-annotated slot stays narrow and is refused with `UnsharedTypeId` — including the idiom hand-written as an interface. An interface annotating the slot as plain `symbol` is structurally identical to the class idiom and is accepted.
18
+
3
19
  ## 8.0.0
4
20
 
5
21
  ### Major Changes
package/README.md CHANGED
@@ -15,8 +15,8 @@ When both channels are inhabited, `Workflow<Command, Decision, Error>` is the fu
15
15
  `(command: Command) => Result<Decision, Error>` carrying the nominal `WorkflowBrand`
16
16
  conjunct — a phantom readonly TypeId-keyed field that no runtime property backs. The brand
17
17
  is what makes the workbook nominal: `Workflow.make` is the only constructor that applies
18
- it, and every surface that runs a decision — the `decide` member a `Cell.layer` spec
19
- demands — requires it, so a decision that skipped `make` is a compile error at the call site that would have run it, with the brand named in the diagnostic. A `never` channel does
18
+ it, and every surface that runs a decision — the chain's `decide` slot demands it
19
+ requires it, so a decision that skipped `make` is a compile error at the call site that would have run it, with the brand named in the diagnostic. A `never` channel does
20
20
  not silently collapse to that function: it resolves to a marker interface that no function
21
21
  can satisfy, so the mistake is a compile error with the remediation attached (below). The
22
22
  success channel is shaped the same way: `Workflow.make` refuses a decision channel that is
@@ -133,22 +133,80 @@ decision anything may run. The error channel is a real variant (`RestartDecision
133
133
  giving up is a decision the caller must branch on, so declaring the error channel `never` is
134
134
  rejected, not allowed.
135
135
 
136
+ ## Building a cell: the `Sandwich` chain
137
+
138
+ A cell is authored as a typed continuation chain, not a record of phases. `Sandwich.read` takes the impure read effect and returns only the lawful next steps; each step composes its `run` at construction and exposes only what may follow, so a misordered chain fails to compile with a missing-method error whose displayed type names the lawful next steps. There is no interpreter: composition happens step by step inside the constructors. Every finished cell carries a recorded `phases` tuple — a type-level literal plus a matching runtime array, both produced by the constructors.
139
+
140
+ | Step | Exposes next | Channel law |
141
+ | -------- | ----------------------------------------------- | ---------------------------------------------------------------- |
142
+ | `read` | `decode`, `decide` | `I` consumed; `Raw` produced; `E`/`R` from the `Effect` |
143
+ | `decode` | `decide` | a `Sandwich.pure` phase; its refusal fails the cell |
144
+ | `decide` | `encode` (decoded chain) or `write` (raw chain) | a `Workflow.make` value; the outcome is a value, never a failure |
145
+ | `encode` | `write` | a `Sandwich.pure` phase shaping the outcome `Result` |
146
+ | `write` | — | `Out` plus `Raw` in (or unary `Out` alone); `Resp`/`E`/`R` out |
147
+
148
+ A short chain skips the filling's middle steps — `read → decide → write` — and records exactly those three phases:
149
+
150
+ ```ts
151
+ import { Sandwich, type Workflow } from '@systemfsoftware/effect-cell-types'
152
+ import { Effect, Result } from 'effect'
153
+
154
+ // The reader's own domain: a command, its raw reading, and a decide outcome.
155
+ interface Command {
156
+ readonly id: string
157
+ }
158
+ interface Raw {
159
+ readonly bytes: string
160
+ }
161
+
162
+ // `admit` is a `Workflow.make` value over the decoded form, built as in
163
+ // "The constructor" above; `render` turns its outcome into a string.
164
+ declare const admit: Workflow<Decoded, Admitted, Malformed>
165
+ declare const render: (outcome: Result.Result<Admitted, Malformed>) => string
166
+
167
+ const cell = Sandwich.read((command: Command) => Effect.succeed(new Decoded({ length: command.id.length }))).decide(
168
+ admit,
169
+ ).write(
170
+ (outcome: Result.Result<Admitted, Malformed>) => Effect.sync(() => render(outcome)),
171
+ )
172
+
173
+ cell.phases // ['read', 'decide', 'write']
174
+ ```
175
+
176
+ A full chain fills `decode` and `encode` with `Sandwich.pure` phases — synchronous `Result`-returning thunks, the only values the slots accept, so no `Effect` can be evaluated inside them:
177
+
178
+ ```ts
179
+ const full = Sandwich.read((command: Command) => Effect.succeed({ bytes: command.id })).decode(
180
+ Sandwich.pure((raw: Raw): Result.Result<Decoded, Malformed> =>
181
+ Result.succeed(new Decoded({ length: raw.bytes.length }))
182
+ ),
183
+ ).decide(admit).encode(
184
+ Sandwich.pure((outcome: Result.Result<Admitted, Malformed>): Result.Result<string, never> =>
185
+ Result.succeed(render(outcome))
186
+ ),
187
+ ).write((line: string, raw: Raw) => Effect.succeed(`${line}<-${raw.bytes}`))
188
+
189
+ full.phases // ['read', 'decode', 'decide', 'encode', 'write']
190
+ ```
191
+
192
+ The `decide` refusal is an outcome, not a failure: it travels to `encode` and `write` as a `Result` value. A `decode` refusal fails the cell and the run stops there.
193
+
136
194
  ## What it rejects at compile time
137
195
 
138
196
  All six violations fail `tsc`; the messages below are what `tsc` reports (verified against this package and `effect@4.0.0-rc.108`).
139
197
 
140
- | Violation | `tsc` reports | Why it is rejected |
141
- | ----------------------------------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
142
- | A `Promise` return | `Type 'Promise<Decision>' is not assignable to type 'Result<Decision, Err>'` | a workflow is a synchronous pure decision; async work belongs in the executor shell around it |
143
- | An `Effect` return | `Type 'Effect<Decision, never, never>' is not assignable to type 'Result<Decision, Err>'` | the workflow returns a value, not an effect handle; the executor runs effects and hands the workflow its input |
144
- | `never` decision channel | `Type '...' is not assignable to type 'UninhabitedDecision'` | a workflow that can never produce a decision can never succeed |
145
- | `never` error channel | `Type '...' is not assignable to type 'UninhabitedError'` | a workflow that cannot fail decides nothing; fold the function into its owning module |
146
- | An untagged error variant | `Type '...' is not assignable to type 'UntaggedError'` | an error variant needs a `_tag` a consumer can dispatch on; declare the errors as `S.TaggedError` instances |
147
- | A single-variant decision channel | `Type '...' is not assignable to type 'SingleVariantDecision'` | a decision chooses between at least two distinguishable outcomes; one variant is a calculation wearing a decision's shape |
148
- | An untagged decision variant | `Type '...' is not assignable to type 'UntaggedDecision'` | a decision variant needs a `_tag` a consumer can dispatch on; declare the variants as `S.TaggedClass` instances |
149
- | Decision variants with no shared TypeId | `Type '...' is not assignable to type 'UnsharedTypeId'` | one decision family carries one TypeId — a `Symbol.for` brand on every variant class |
150
- | A bare decider in a `Cell.layer` spec | `Type '(command: Cmd) => Result<Dec, Err>' is not assignable to type 'WorkflowBrand'` | only a `Workflow.make` value satisfies the `decide` member; a lambda that skipped `make` is not a decision a description may run |
151
- | A plain interface at the command position | `'Cmd' only refers to a type, but is being used as a value here` | the command is constrained on the value, and a declared type produces none — so there is no marker to smuggle |
198
+ | Violation | `tsc` reports | Why it is rejected |
199
+ | ----------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
200
+ | A `Promise` return | `Type 'Promise<Decision>' is not assignable to type 'Result<Decision, Err>'` | a workflow is a synchronous pure decision; async work belongs in the executor shell around it |
201
+ | An `Effect` return | `Type 'Effect<Decision, never, never>' is not assignable to type 'Result<Decision, Err>'` | the workflow returns a value, not an effect handle; the executor runs effects and hands the workflow its input |
202
+ | `never` decision channel | `Type '...' is not assignable to type 'UninhabitedDecision'` | a workflow that can never produce a decision can never succeed |
203
+ | `never` error channel | `Type '...' is not assignable to type 'UninhabitedError'` | a workflow that cannot fail decides nothing; fold the function into its owning module |
204
+ | An untagged error variant | `Type '...' is not assignable to type 'UntaggedError'` | an error variant needs a `_tag` a consumer can dispatch on; declare the errors as `S.TaggedError` instances |
205
+ | A single-variant decision channel | `Type '...' is not assignable to type 'SingleVariantDecision'` | a decision chooses between at least two distinguishable outcomes; one variant is a calculation wearing a decision's shape |
206
+ | An untagged decision variant | `Type '...' is not assignable to type 'UntaggedDecision'` | a decision variant needs a `_tag` a consumer can dispatch on; declare the variants as `S.TaggedClass` instances |
207
+ | Decision variants with no shared TypeId | `Type '...' is not assignable to type 'UnsharedTypeId'` | one decision family carries one TypeId — a `Symbol.for` brand on every variant class |
208
+ | A bare decider in a `decide` slot | `Type '(command: Cmd) => Result<Dec, Err>' is not assignable to type 'WorkflowBrand'` | only a `Workflow.make` value satisfies the `decide` slot; a lambda that skipped `make` is not a decision a chain may run |
209
+ | A plain interface at the command position | `'Cmd' only refers to a type, but is being used as a value here` | the command is constrained on the value, and a declared type produces none — so there is no marker to smuggle |
152
210
 
153
211
  The two `never` cases are where the content-vs-filename distinction pays off. `Workflow<C, never, E>` resolves to `UninhabitedDecision` and `Workflow<C, D, never>` to `UninhabitedError` — interfaces whose only property is required and whose _type_ is the remediation, so the compile error points at the fix:
154
212
 
package/dist/index.d.ts CHANGED
@@ -5,89 +5,11 @@ import { Result } from "effect/Result";
5
5
  import { Kind, TypeLambda } from "effect/HKT";
6
6
  import { Layer } from "effect/Layer";
7
7
  import * as Schema from "effect/Schema";
8
- //#region src/Facts.d.ts
9
- declare const DESCRIPTION_MODULE: '@systemfsoftware/effect-cell-types';
10
- declare const IO_CELLS: {
11
- readonly cells: readonly ['store', 'adapter'];
12
- readonly sources: readonly ['effect/Clock', 'effect/System'];
13
- };
14
- type IoCellClassification = typeof IO_CELLS;
15
- type PhaseName = 'read' | 'decode' | 'decide' | 'encode' | 'write';
16
- declare namespace Workflow_d_exports {
17
- export { Inhabited, SingleVariantDecision, UninhabitedDecision, UninhabitedError, UnsharedTypeId, UntaggedDecision, UntaggedError, Workflow, WorkflowBrand, andThen$1 as andThen, make, total };
18
- }
19
- declare const WorkflowTypeId: unique symbol;
20
- type WorkflowTypeId = typeof WorkflowTypeId;
21
- interface WorkflowBrand {
22
- readonly [WorkflowTypeId]: WorkflowTypeId;
23
- }
24
- interface UninhabitedDecision {
25
- readonly __WORKFLOW_DECISION_CHANNEL_IS_NEVER__: 'this workflow can never succeed; give it a decision variant it can return';
26
- }
27
- interface UninhabitedError {
28
- readonly __WORKFLOW_ERROR_CHANNEL_IS_NEVER__: 'this workflow cannot fail, so it decides nothing; give it an error variant or fold the function into its owning module';
29
- }
30
- interface UntaggedError {
31
- readonly __WORKFLOW_ERROR_CHANNEL_CARRIES_NO_TAG__: 'this error carries no _tag the consumer can dispatch on; declare it as an S.TaggedError';
32
- }
33
- interface SingleVariantDecision {
34
- readonly __WORKFLOW_DECISION_CHANNEL_HAS_ONE_VARIANT__: 'this workflow decides one outcome, which is not a decision; add the variant it chooses between, or fold the function into its owning module';
35
- }
36
- interface UntaggedDecision {
37
- readonly __WORKFLOW_DECISION_CHANNEL_CARRIES_NO_TAG__: 'a decision variant carries no _tag the consumer can dispatch on; declare the variants as S.TaggedClass instances';
38
- }
39
- interface UnsharedTypeId {
40
- readonly __WORKFLOW_DECISION_VARIANTS_DO_NOT_SHARE_A_TYPE_ID__: 'the decision variants must share one TypeId — a Symbol.for family brand on each variant class';
41
- }
42
- type AtLeastTwoDistinct<T, U = T> = U extends unknown ? [T] extends [U] ? false : true : never;
43
- type TaggedMembers<D> = D extends unknown ? '_tag' extends keyof D ? [D['_tag']] extends [string] ? true : false : false : never;
44
- type MutuallyAssignable<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
45
- type BrandSlotIsTheGeneralSymbol<D, K extends PropertyKey> = D extends unknown ? K extends keyof D ? MutuallyAssignable<D[K], symbol> : false : never;
46
- type SharedTypeId<D> = [{ [K in keyof D]: [K] extends [symbol] ? ([BrandSlotIsTheGeneralSymbol<D, K>] extends [true] ? K : never) : never; }[keyof D]] extends [never] ? UnsharedTypeId : unknown;
47
- type DecisionShape<D> = [unknown] extends [D] ? unknown : AtLeastTwoDistinct<D> extends false ? SingleVariantDecision : boolean extends TaggedMembers<D> ? UntaggedDecision : SharedTypeId<D>;
48
- type Workflow<Command, Decision, DecisionError> = [Decision] extends [never] ? UninhabitedDecision : [DecisionError] extends [never] ? UninhabitedError : ((command: Command) => Result<Decision, DecisionError>) & WorkflowBrand;
49
- type DispatchableTag<E> = '_tag' extends keyof E ? [E['_tag']] extends [string] ? unknown : UntaggedError : UntaggedError;
50
- type Inhabited<Decision, DecisionError> = [Decision] extends [never] ? UninhabitedDecision : [DecisionError] extends [never] ? UninhabitedError : DecisionShape<Decision> & DispatchableTag<DecisionError>;
51
- declare const make: <Self, S extends Schema.Constraint & {
52
- readonly fields: Schema.Struct.Fields;
53
- }, Inherited, D, E>(_command: Schema.Class<Self, S, Inherited>, decide: (command: Self) => Result<D, E> & Inhabited<D, E>) => Workflow<Self, D, E>;
54
- /**
55
- * Brands a decision that cannot fail. `make` refuses a `never` error channel outright
56
- * (`UninhabitedError`); this is the door for the decider that genuinely decides everything.
57
- * The decision must still choose between at least two tagged variants sharing one TypeId, so
58
- * `SingleVariantDecision`, `UntaggedDecision`, and `UnsharedTypeId` still fire.
59
- *
60
- * The command schema class comes first, exactly as in {@link make}, so the command channel
61
- * stays pinned to the class rather than an inferred annotation. The decider's return carries
62
- * `DecisionShape` written out: the `Workflow` alias is a deferred conditional, and in
63
- * parameter position it collapses the whole parameter to `unknown` while the decision
64
- * channel is still generic.
65
- */
66
- declare const total: <Self, S extends Schema.Constraint & {
67
- readonly fields: Schema.Struct.Fields;
68
- }, Inherited, D>(_command: Schema.Class<Self, S, Inherited>, decide: (command: Self) => Result<D, never> & DecisionShape<D>) => ((command: Self) => Result<D, never>) & WorkflowBrand;
69
- /**
70
- * Composes two workflows: what the upstream decides becomes the command the downstream decides
71
- * on, and a refusal short-circuits the pair. The return type dispatches on the component error
72
- * union — two components that cannot fail publish the total form, because the `Workflow` alias
73
- * refuses a `never` channel; a carried error publishes the union as before.
74
- */
75
- declare const andThen$1: <Ctx, SelfA, SA extends Schema.Constraint & {
76
- readonly fields: Schema.Struct.Fields;
77
- }, InheritedA, D1, E1, SelfB extends {
78
- readonly decision: D1;
79
- readonly ctx: Ctx;
80
- }, D2, E2>(_commandA: Schema.Class<SelfA, SA, InheritedA>, upstream: ((command: SelfA) => Result<D1, E1>) & WorkflowBrand, commandB: {
81
- new (props: {
82
- readonly decision: D1;
83
- readonly ctx: Ctx;
84
- }): SelfB;
85
- }, ctx: NoInfer<Ctx>, downstream: ((command: SelfB) => Result<D2, E2>) & WorkflowBrand) => [E1 | E2] extends [never] ? ((command: SelfA) => Result<D2, never>) & WorkflowBrand : Workflow<SelfA, D2, E1 | E2>;
86
8
  declare namespace Cell_d_exports {
87
- export { Cell, CellTypeId, DESCRIPTION_MODULE, IO_CELLS, IoCellClassification, Kind$1 as Kind, PhaseName, Run, TypeLambda$1 as TypeLambda, Vocabulary, andThen, collect, collectAll, gate, layer, map, mapInput, provide, vocabulary, zip };
9
+ export { Cell, CellTypeId, Kind$1 as Kind, Run, TypeLambda$1 as TypeLambda, andThen$1 as andThen, collect, collectAll, gate, map, mapInput, provide, zip };
88
10
  }
89
11
  /**
90
- * The nominal brand every `Cell` carries. `Cell.layer` is the only door that applies it.
12
+ * The nominal brand every `Cell` carries. The `Sandwich` chain's `write` is the only door that applies it.
91
13
  */
92
14
  declare const CellTypeId: unique symbol;
93
15
  /**
@@ -120,48 +42,6 @@ type Kind$1<I, E, R, A> = Kind<TypeLambda$1, I, E, R, A>;
120
42
  * `Run<I, A, E, R>` is `Cell<I, A, E, R>['run']`.
121
43
  */
122
44
  type Run<I, A, E, R> = Cell<I, A, E, R>['run'];
123
- interface LayerCore<I, Raw, RE, RR, Dec, DE, Resp, WE, WR> {
124
- readonly read: (command: I) => Effect.Effect<Raw, RE, RR>;
125
- readonly decide: ((decoded: Raw) => Result$1.Result<Dec, DE>) & WorkflowBrand;
126
- readonly write: (output: Result$1.Result<Dec, DE>, raw: Raw) => Effect.Effect<Resp, WE, WR>;
127
- }
128
- interface LayerShortSpec<I, Raw, RE, RR, Dec, DE, Resp, WE, WR> extends LayerCore<I, Raw, RE, RR, Dec, DE, Resp, WE, WR> {
129
- readonly decode?: never;
130
- readonly encode?: never;
131
- }
132
- interface LayerLongSpec<I, Raw, RE, RR, Dcd, DecE, Dec, DE, Out, Resp, WE, WR> extends Omit<LayerCore<I, Raw, RE, RR, Dec, DE, Resp, WE, WR>, 'decide' | 'write'> {
133
- readonly decode: (raw: Raw) => Result$1.Result<Dcd, DecE>;
134
- readonly decide: ((decoded: Dcd) => Result$1.Result<Dec, DE>) & WorkflowBrand;
135
- readonly encode: (outcome: Result$1.Result<Dec, DE>) => Out;
136
- readonly write: (output: Out, raw: Raw) => Effect.Effect<Resp, WE, WR>;
137
- }
138
- /**
139
- * Builds a Cell from one sandwich.
140
- *
141
- * Short form — `read` produces the value `decide` rules on, and the decide outcome is what
142
- * `write` receives:
143
- *
144
- * ```ts
145
- * import { Cell, Workflow } from '@systemfsoftware/effect-cell-types'
146
- * import { Effect, Result } from 'effect'
147
- *
148
- * declare const decideAdmission: Workflow<CliArgs, Verdict, Refusal>
149
- * declare class CliArgs { readonly target: string }
150
- * declare class Verdict { readonly ok: boolean }
151
- * declare class Refusal { readonly _tag: 'Refused' }
152
- *
153
- * const cell = Cell.layer({
154
- * read: (args: CliArgs) => Effect.succeed(args),
155
- * decide: decideAdmission,
156
- * write: (outcome: Result.Result<Verdict, Refusal>, raw: CliArgs) => Effect.void,
157
- * })
158
- * ```
159
- *
160
- * Long form — `decode` and `encode` adapt each side of `decide`; both are required together,
161
- * and a spec carrying one without the other fails inference.
162
- */
163
- declare function layer<I, Raw, RE, RR, Dec, DE, Resp, WE, WR>(spec: LayerShortSpec<I, Raw, RE, RR, Dec, DE, Resp, WE, WR>): Cell<I, Resp, RE | WE, RR | WR>;
164
- declare function layer<I, Raw, RE, RR, Dcd, DecE, Dec, DE, Out, Resp, WE, WR>(spec: LayerLongSpec<I, Raw, RE, RR, Dcd, DecE, Dec, DE, Out, Resp, WE, WR>): Cell<I, Resp, RE | DecE | WE, RR | WR>;
165
45
  /**
166
46
  * Transforms the Cell's response.
167
47
  */
@@ -180,7 +60,7 @@ declare const mapInput: {
180
60
  * Feeds this Cell's response to the next Cell as its input. The error and service channels
181
61
  * union.
182
62
  */
183
- declare const andThen: {
63
+ declare const andThen$1: {
184
64
  <B, E2, R2>(that: Cell<never, B, E2, R2>): <I, A, E, R>(self: Cell<I, A, E, R>) => Cell<I, B, E | E2, R | R2>;
185
65
  <I, A, E, R, B, E2, R2>(self: Cell<I, A, E, R>, that: Cell<A, B, E2, R2>): Cell<I, B, E | E2, R | R2>;
186
66
  };
@@ -226,19 +106,110 @@ declare const provide: {
226
106
  <RIn, LE, ROut>(layer: Layer<ROut, LE, RIn>): <I, A, E, R>(self: Cell<I, A, E, R>) => Cell<I, A, E | LE, RIn | Exclude<R, ROut>>;
227
107
  <I, A, E, R, RIn, LE, ROut>(self: Cell<I, A, E, R>, layer: Layer<ROut, LE, RIn>): Cell<I, A, E | LE, RIn | Exclude<R, ROut>>;
228
108
  };
109
+ declare namespace Workflow_d_exports {
110
+ export { Inhabited, SingleVariantDecision, UninhabitedDecision, UninhabitedError, UnsharedTypeId, UntaggedDecision, UntaggedError, Workflow, WorkflowBrand, andThen, make, total };
111
+ }
112
+ declare const WorkflowTypeId: unique symbol;
113
+ type WorkflowTypeId = typeof WorkflowTypeId;
114
+ interface WorkflowBrand {
115
+ readonly [WorkflowTypeId]: WorkflowTypeId;
116
+ }
117
+ interface UninhabitedDecision {
118
+ readonly __WORKFLOW_DECISION_CHANNEL_IS_NEVER__: 'this workflow can never succeed; give it a decision variant it can return';
119
+ }
120
+ interface UninhabitedError {
121
+ readonly __WORKFLOW_ERROR_CHANNEL_IS_NEVER__: 'this workflow cannot fail, so it decides nothing; give it an error variant or fold the function into its owning module';
122
+ }
123
+ interface UntaggedError {
124
+ readonly __WORKFLOW_ERROR_CHANNEL_CARRIES_NO_TAG__: 'this error carries no _tag the consumer can dispatch on; declare it as an S.TaggedError';
125
+ }
126
+ interface SingleVariantDecision {
127
+ readonly __WORKFLOW_DECISION_CHANNEL_HAS_ONE_VARIANT__: 'this workflow decides one outcome, which is not a decision; add the variant it chooses between, or fold the function into its owning module';
128
+ }
129
+ interface UntaggedDecision {
130
+ readonly __WORKFLOW_DECISION_CHANNEL_CARRIES_NO_TAG__: 'a decision variant carries no _tag the consumer can dispatch on; declare the variants as S.TaggedClass instances';
131
+ }
132
+ interface UnsharedTypeId {
133
+ readonly __WORKFLOW_DECISION_VARIANTS_DO_NOT_SHARE_A_TYPE_ID__: 'the decision variants must share one TypeId — a Symbol.for family brand on each variant class';
134
+ }
135
+ type AtLeastTwoDistinct<T, U = T> = U extends unknown ? [T] extends [U] ? false : true : never;
136
+ type TaggedMembers<D> = D extends unknown ? '_tag' extends keyof D ? [D['_tag']] extends [string] ? true : false : false : never;
137
+ type MutuallyAssignable<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
138
+ type BrandSlotIsTheGeneralSymbol<D, K extends PropertyKey> = D extends unknown ? K extends keyof D ? MutuallyAssignable<D[K], symbol> : false : never;
139
+ type SharedTypeId<D> = [{ [K in keyof D]: [K] extends [symbol] ? ([BrandSlotIsTheGeneralSymbol<D, K>] extends [true] ? K : never) : never; }[keyof D]] extends [never] ? UnsharedTypeId : unknown;
140
+ type DecisionShape<D> = [unknown] extends [D] ? unknown : AtLeastTwoDistinct<D> extends false ? SingleVariantDecision : boolean extends TaggedMembers<D> ? UntaggedDecision : SharedTypeId<D>;
141
+ type Workflow<Command, Decision, DecisionError> = [Decision] extends [never] ? UninhabitedDecision : [DecisionError] extends [never] ? UninhabitedError : ((command: Command) => Result<Decision, DecisionError>) & WorkflowBrand;
142
+ type DispatchableTag<E> = '_tag' extends keyof E ? [E['_tag']] extends [string] ? unknown : UntaggedError : UntaggedError;
143
+ type Inhabited<Decision, DecisionError> = [Decision] extends [never] ? UninhabitedDecision : [DecisionError] extends [never] ? UninhabitedError : DecisionShape<Decision> & DispatchableTag<DecisionError>;
144
+ declare const make: <Self, S extends Schema.Constraint & {
145
+ readonly fields: Schema.Struct.Fields;
146
+ }, Inherited, D, E>(_command: Schema.Class<Self, S, Inherited>, decide: (command: Self) => Result<D, E> & Inhabited<D, E>) => Workflow<Self, D, E>;
229
147
  /**
230
- * The facts the lint plugin judges a spec body by, as a const table. The order the
231
- * interpreter runs is the text of {@link layerRunner}; the table states only what a
232
- * rule cannot read off a type: which phases are pure, and what counts as I/O.
148
+ * Brands a decision that cannot fail. `make` refuses a `never` error channel outright
149
+ * (`UninhabitedError`); this is the door for the decider that genuinely decides everything.
150
+ * The decision must still choose between at least two tagged variants sharing one TypeId, so
151
+ * `SingleVariantDecision`, `UntaggedDecision`, and `UnsharedTypeId` still fire.
152
+ *
153
+ * The command schema class comes first, exactly as in {@link make}, so the command channel
154
+ * stays pinned to the class rather than an inferred annotation. The decider's return carries
155
+ * `DecisionShape` written out: the `Workflow` alias is a deferred conditional, and in
156
+ * parameter position it collapses the whole parameter to `unknown` while the decision
157
+ * channel is still generic.
158
+ */
159
+ declare const total: <Self, S extends Schema.Constraint & {
160
+ readonly fields: Schema.Struct.Fields;
161
+ }, Inherited, D>(_command: Schema.Class<Self, S, Inherited>, decide: (command: Self) => Result<D, never> & DecisionShape<D>) => ((command: Self) => Result<D, never>) & WorkflowBrand;
162
+ /**
163
+ * Composes two workflows: what the upstream decides becomes the command the downstream decides
164
+ * on, and a refusal short-circuits the pair. The return type dispatches on the component error
165
+ * union — two components that cannot fail publish the total form, because the `Workflow` alias
166
+ * refuses a `never` channel; a carried error publishes the union as before.
233
167
  */
234
- interface Vocabulary {
235
- readonly module: typeof DESCRIPTION_MODULE;
236
- readonly ioCells: IoCellClassification;
237
- readonly byKind: {
238
- readonly pure: readonly PhaseName[];
168
+ declare const andThen: <Ctx, SelfA, SA extends Schema.Constraint & {
169
+ readonly fields: Schema.Struct.Fields;
170
+ }, InheritedA, D1, E1, SelfB extends {
171
+ readonly decision: D1;
172
+ readonly ctx: Ctx;
173
+ }, D2, E2>(_commandA: Schema.Class<SelfA, SA, InheritedA>, upstream: ((command: SelfA) => Result<D1, E1>) & WorkflowBrand, commandB: {
174
+ new (props: {
175
+ readonly decision: D1;
176
+ readonly ctx: Ctx;
177
+ }): SelfB;
178
+ }, ctx: NoInfer<Ctx>, downstream: ((command: SelfB) => Result<D2, E2>) & WorkflowBrand) => [E1 | E2] extends [never] ? ((command: SelfA) => Result<D2, never>) & WorkflowBrand : Workflow<SelfA, D2, E1 | E2>;
179
+ declare namespace Sandwich_d_exports {
180
+ export { DecodedChain, DecodedDecidedChain, EncodedChain, PurePhase, RawDecidedChain, ReadChain, pure, read };
181
+ }
182
+ declare const PurePhaseBrand: unique symbol;
183
+ type PurePhaseBrand = typeof PurePhaseBrand;
184
+ type PurePhase<In, Out, E = never> = ((input: In) => Result$1.Result<Out, E>) & {
185
+ readonly [PurePhaseBrand]: true;
186
+ };
187
+ declare const pure: <In, Out, E = never>(fn: (input: In) => Result$1.Result<Out, E>) => PurePhase<In, Out, E>;
188
+ interface ReadChain<I, Raw, RE, RR> {
189
+ readonly 'sentence: must decode or decide after read': true;
190
+ decode<Dcd, DecE>(phase: PurePhase<Raw, Dcd, DecE>): DecodedChain<I, Raw, Dcd, RE, DecE, RR>;
191
+ decide<Dec, DE>(workflow: ((decoded: Raw) => Result$1.Result<Dec, DE>) & WorkflowBrand): RawDecidedChain<I, Raw, Dec, DE, RE, RR>;
192
+ }
193
+ interface DecodedChain<I, Raw, Dcd, RE, DecE, RR> {
194
+ readonly 'sentence: must decide after decode': true;
195
+ decide<Dec, DE>(workflow: ((decoded: Dcd) => Result$1.Result<Dec, DE>) & WorkflowBrand): DecodedDecidedChain<I, Raw, Dec, DE, RE, DecE, RR>;
196
+ }
197
+ interface RawDecidedChain<I, Raw, Dec, DE, RE, RR> {
198
+ readonly 'sentence: must write after decide on raw chain': true;
199
+ write<Resp, WE, WR>(run: (output: Result$1.Result<Dec, DE>, raw: Raw) => Effect.Effect<Resp, WE, WR>): Cell<I, Resp, RE | WE, RR | WR> & {
200
+ readonly phases: readonly ['read', 'decide', 'write'];
201
+ };
202
+ }
203
+ interface DecodedDecidedChain<I, Raw, Dec, DE, RE, DecE, RR> {
204
+ readonly 'sentence: must encode after decide on decoded chain': true;
205
+ encode<Out>(phase: PurePhase<Result$1.Result<Dec, DE>, Out, never>): EncodedChain<I, Raw, Out, RE, DecE, RR>;
206
+ }
207
+ interface EncodedChain<I, Raw, Out, RE, DecE, RR> {
208
+ readonly 'sentence: must write after encode': true;
209
+ write<Resp, WE, WR>(run: (output: Out, raw: Raw) => Effect.Effect<Resp, WE, WR>): Cell<I, Resp, RE | DecE | WE, RR | WR> & {
210
+ readonly phases: readonly ['read', 'decode', 'decide', 'encode', 'write'];
239
211
  };
240
- readonly composer: 'layer';
241
212
  }
242
- declare const vocabulary: Vocabulary;
213
+ declare const read: <I, Raw, RE, RR>(run: (command: I) => Effect.Effect<Raw, RE, RR>) => ReadChain<I, Raw, RE, RR>;
243
214
  //#endregion
244
- export { Cell_d_exports as Cell, Workflow_d_exports as Workflow };
215
+ export { Cell_d_exports as Cell, Sandwich_d_exports as Sandwich, Workflow_d_exports as Workflow };
package/dist/index.mjs CHANGED
@@ -4,122 +4,49 @@ import { dual } from "effect/Function";
4
4
  import * as Option from "effect/Option";
5
5
  import * as Result$1 from "effect/Result";
6
6
  import { flatMap } from "effect/Result";
7
- //#region src/Facts.ts
8
- const DESCRIPTION_MODULE = "@systemfsoftware/effect-cell-types";
9
- const IO_CELLS = {
10
- cells: ["store", "adapter"],
11
- sources: ["effect/Clock", "effect/System"]
12
- };
13
- //#endregion
14
- //#region src/Workflow.ts
15
- var Workflow_exports = /* @__PURE__ */ __exportAll({
16
- andThen: () => andThen$1,
17
- make: () => make$1,
18
- total: () => total
19
- });
20
- const make$1 = (_command, decide) => {
21
- return decide;
22
- };
23
- /**
24
- * Brands a decision that cannot fail. `make` refuses a `never` error channel outright
25
- * (`UninhabitedError`); this is the door for the decider that genuinely decides everything.
26
- * The decision must still choose between at least two tagged variants sharing one TypeId, so
27
- * `SingleVariantDecision`, `UntaggedDecision`, and `UnsharedTypeId` still fire.
28
- *
29
- * The command schema class comes first, exactly as in {@link make}, so the command channel
30
- * stays pinned to the class rather than an inferred annotation. The decider's return carries
31
- * `DecisionShape` written out: the `Workflow` alias is a deferred conditional, and in
32
- * parameter position it collapses the whole parameter to `unknown` while the decision
33
- * channel is still generic.
34
- */
35
- const total = (_command, decide) => {
36
- return decide;
37
- };
38
- /**
39
- * Composes two workflows: what the upstream decides becomes the command the downstream decides
40
- * on, and a refusal short-circuits the pair. The return type dispatches on the component error
41
- * union — two components that cannot fail publish the total form, because the `Workflow` alias
42
- * refuses a `never` channel; a carried error publishes the union as before.
43
- */
44
- const andThen$1 = (_commandA, upstream, commandB, ctx, downstream) => {
45
- const composed = (command) => flatMap(upstream(command), (decision) => downstream(new commandB({
46
- decision,
47
- ctx
48
- })));
49
- return composed;
50
- };
51
- //#endregion
52
7
  //#region src/Cell.ts
53
8
  var Cell_exports = /* @__PURE__ */ __exportAll({
54
9
  CellTypeId: () => CellTypeId,
55
- DESCRIPTION_MODULE: () => DESCRIPTION_MODULE,
56
- IO_CELLS: () => IO_CELLS,
57
- andThen: () => andThen,
10
+ andThen: () => andThen$1,
58
11
  collect: () => collect,
59
12
  collectAll: () => collectAll,
60
13
  gate: () => gate,
61
- layer: () => layer,
62
14
  map: () => map,
63
15
  mapInput: () => mapInput,
64
16
  provide: () => provide,
65
- vocabulary: () => vocabulary,
66
17
  zip: () => zip
67
18
  });
68
19
  /**
69
- * The nominal brand every `Cell` carries. `Cell.layer` is the only door that applies it.
20
+ * The nominal brand every `Cell` carries. The `Sandwich` chain's `write` is the only door that applies it.
70
21
  */
71
22
  const CellTypeId = Symbol.for("@systemfsoftware/effect-cell-types/Cell");
72
- const make = (run) => ({
23
+ const make$1 = (run) => ({
73
24
  [CellTypeId]: CellTypeId,
74
25
  run
75
26
  });
76
27
  /**
77
- * The interpreter. Order is the text: read, then decode, then decide, then encode, then
78
- * write. The `E` channel is the sandwich's truth — read, decode, and write failures;
79
- * a decide refusal is the outcome the encode and write receive, not a failure.
80
- */
81
- const layerRunner = (spec) => {
82
- if ("decode" in spec && "encode" in spec) return (input) => Effect.gen(function* () {
83
- const raw = yield* spec.read(input);
84
- const decoded = yield* Result$1.match(spec.decode(raw), {
85
- onFailure: Effect.fail,
86
- onSuccess: Effect.succeed
87
- });
88
- const outcome = spec.decide(decoded);
89
- return yield* spec.write(spec.encode(outcome), raw);
90
- });
91
- return (input) => Effect.gen(function* () {
92
- const raw = yield* spec.read(input);
93
- const outcome = spec.decide(raw);
94
- return yield* spec.write(outcome, raw);
95
- });
96
- };
97
- function layer(spec) {
98
- return make(layerRunner(spec));
99
- }
100
- /**
101
28
  * Transforms the Cell's response.
102
29
  */
103
- const map = dual(2, (self, f) => make((input) => Effect.map(self.run(input), f)));
30
+ const map = dual(2, (self, f) => make$1((input) => Effect.map(self.run(input), f)));
104
31
  /**
105
32
  * Transforms the Cell's input.
106
33
  */
107
- const mapInput = dual(2, (self, f) => make((input) => self.run(f(input))));
34
+ const mapInput = dual(2, (self, f) => make$1((input) => self.run(f(input))));
108
35
  /**
109
36
  * Feeds this Cell's response to the next Cell as its input. The error and service channels
110
37
  * union.
111
38
  */
112
- const andThen = dual(2, (self, that) => make((input) => Effect.flatMap(self.run(input), (response) => that.run(response))));
39
+ const andThen$1 = dual(2, (self, that) => make$1((input) => Effect.flatMap(self.run(input), (response) => that.run(response))));
113
40
  /**
114
41
  * Runs both Cells against the same input and tuples the responses. Fails fast: when one
115
42
  * side refuses, the other's write never runs.
116
43
  */
117
- const zip = dual(2, (self, that) => make((input) => Effect.zipWith(self.run(input), that.run(input), (a, b) => [a, b])));
44
+ const zip = dual(2, (self, that) => make$1((input) => Effect.zipWith(self.run(input), that.run(input), (a, b) => [a, b])));
118
45
  /**
119
46
  * Runs the inner Cell on the value this Cell read, yielding `Option.none` when it read none.
120
47
  * A skip is an absence, never a refusal; the error and service channels union.
121
48
  */
122
- const gate = dual(2, (self, inner) => make((input) => Effect.flatMap(self.run(input), (read) => Option.match(read, {
49
+ const gate = dual(2, (self, inner) => make$1((input) => Effect.flatMap(self.run(input), (read) => Option.match(read, {
123
50
  onNone: () => Effect.succeed(Option.none()),
124
51
  onSome: (raw) => Effect.map(inner.run(raw), Option.some)
125
52
  }))));
@@ -127,28 +54,131 @@ const gate = dual(2, (self, inner) => make((input) => Effect.flatMap(self.run(in
127
54
  * Runs the Cell once per item, in order, and folds the responses into one value. The error
128
55
  * and service channels are unchanged; the first refusal ends the run.
129
56
  */
130
- const collect = dual(2, (self, fold) => make((items) => Effect.map(Effect.forEach(items, (item) => self.run(item)), (responses) => fold(responses))));
57
+ const collect = dual(2, (self, fold) => make$1((items) => Effect.map(Effect.forEach(items, (item) => self.run(item)), (responses) => fold(responses))));
131
58
  /**
132
59
  * Runs the Cell once per item, in order, and folds every outcome — each a `Result.Result` —
133
60
  * into one value. Unlike {@link collect}, no refusal ends the run: every item is attempted
134
61
  * and its failure travels to the fold.
135
62
  */
136
- const collectAll = dual(2, (self, fold) => make((items) => Effect.map(Effect.forEach(items, (item) => Effect.result(self.run(item))), (results) => fold(results))));
63
+ const collectAll = dual(2, (self, fold) => make$1((items) => Effect.map(Effect.forEach(items, (item) => Effect.result(self.run(item))), (results) => fold(results))));
137
64
  /**
138
65
  * Provides a Layer to the Cell, eliminating the services the layer builds from `R`. This is
139
66
  * the one composition-root elimination; the resulting Cell still demands the layer's input
140
67
  * services. A missing provide is a compile error at the run site.
141
68
  */
142
- const provide = dual(2, (self, layer) => make((input) => Effect.provide(self.run(input), layer)));
143
- const vocabulary = {
144
- module: DESCRIPTION_MODULE,
145
- ioCells: IO_CELLS,
146
- byKind: { pure: [
147
- "decode",
148
- "decide",
149
- "encode"
150
- ] },
151
- composer: "layer"
69
+ const provide = dual(2, (self, layer) => make$1((input) => Effect.provide(self.run(input), layer)));
70
+ //#endregion
71
+ //#region src/Workflow.ts
72
+ var Workflow_exports = /* @__PURE__ */ __exportAll({
73
+ andThen: () => andThen,
74
+ make: () => make,
75
+ total: () => total
76
+ });
77
+ const make = (_command, decide) => {
78
+ return decide;
79
+ };
80
+ /**
81
+ * Brands a decision that cannot fail. `make` refuses a `never` error channel outright
82
+ * (`UninhabitedError`); this is the door for the decider that genuinely decides everything.
83
+ * The decision must still choose between at least two tagged variants sharing one TypeId, so
84
+ * `SingleVariantDecision`, `UntaggedDecision`, and `UnsharedTypeId` still fire.
85
+ *
86
+ * The command schema class comes first, exactly as in {@link make}, so the command channel
87
+ * stays pinned to the class rather than an inferred annotation. The decider's return carries
88
+ * `DecisionShape` written out: the `Workflow` alias is a deferred conditional, and in
89
+ * parameter position it collapses the whole parameter to `unknown` while the decision
90
+ * channel is still generic.
91
+ */
92
+ const total = (_command, decide) => {
93
+ return decide;
94
+ };
95
+ /**
96
+ * Composes two workflows: what the upstream decides becomes the command the downstream decides
97
+ * on, and a refusal short-circuits the pair. The return type dispatches on the component error
98
+ * union — two components that cannot fail publish the total form, because the `Workflow` alias
99
+ * refuses a `never` channel; a carried error publishes the union as before.
100
+ */
101
+ const andThen = (_commandA, upstream, commandB, ctx, downstream) => {
102
+ const composed = (command) => flatMap(upstream(command), (decision) => downstream(new commandB({
103
+ decision,
104
+ ctx
105
+ })));
106
+ return composed;
107
+ };
108
+ //#endregion
109
+ //#region src/Sandwich.ts
110
+ var Sandwich_exports = /* @__PURE__ */ __exportAll({
111
+ pure: () => pure,
112
+ read: () => read
113
+ });
114
+ const PurePhaseBrand = Symbol.for("@systemfsoftware/effect-cell-types/PurePhase");
115
+ const pure = (fn) => Object.assign(fn, { [PurePhaseBrand]: true });
116
+ const read = (run) => {
117
+ const decode = (phase) => {
118
+ const decide = (workflow) => {
119
+ const encode = (encodePhase) => {
120
+ const write = (writeRun) => {
121
+ const composed = (input) => Effect.gen(function* () {
122
+ const raw = yield* run(input);
123
+ const outcome = workflow(yield* Result$1.match(phase(raw), {
124
+ onFailure: Effect.fail,
125
+ onSuccess: Effect.succeed
126
+ }));
127
+ return yield* writeRun(Result$1.getOrThrow(encodePhase(outcome)), raw);
128
+ });
129
+ return {
130
+ [CellTypeId]: CellTypeId,
131
+ run: composed,
132
+ phases: [
133
+ "read",
134
+ "decode",
135
+ "decide",
136
+ "encode",
137
+ "write"
138
+ ]
139
+ };
140
+ };
141
+ return {
142
+ "sentence: must write after encode": true,
143
+ write
144
+ };
145
+ };
146
+ return {
147
+ "sentence: must encode after decide on decoded chain": true,
148
+ encode
149
+ };
150
+ };
151
+ return {
152
+ "sentence: must decide after decode": true,
153
+ decide
154
+ };
155
+ };
156
+ const decide = (workflow) => {
157
+ const write = (writeRun) => {
158
+ const composed = (input) => Effect.gen(function* () {
159
+ const raw = yield* run(input);
160
+ return yield* writeRun(workflow(raw), raw);
161
+ });
162
+ return {
163
+ [CellTypeId]: CellTypeId,
164
+ run: composed,
165
+ phases: [
166
+ "read",
167
+ "decide",
168
+ "write"
169
+ ]
170
+ };
171
+ };
172
+ return {
173
+ "sentence: must write after decide on raw chain": true,
174
+ write
175
+ };
176
+ };
177
+ return {
178
+ "sentence: must decode or decide after read": true,
179
+ decode,
180
+ decide
181
+ };
152
182
  };
153
183
  //#endregion
154
- export { Cell_exports as Cell, Workflow_exports as Workflow };
184
+ export { Cell_exports as Cell, Sandwich_exports as Sandwich, Workflow_exports as Workflow };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@systemfsoftware/effect-cell-types",
3
3
  "license": "Apache-2.0",
4
- "version": "8.0.0",
4
+ "version": "8.2.0",
5
5
  "author": "Ryan Lee <drdgvhbh@gmail.com>",
6
6
  "repository": {
7
7
  "type": "git",
@@ -34,7 +34,7 @@
34
34
  "devDependencies": {
35
35
  "@effect/vitest": "4.0.0-rc.112",
36
36
  "@microsoft/api-extractor": "^7.58.7",
37
- "@systemfsoftware/arethetypeswrong-cli": "^4.1.0",
37
+ "@systemfsoftware/arethetypeswrong-cli": "^4.2.0",
38
38
  "@types/node": "^24",
39
39
  "effect": "4.0.0-rc.112",
40
40
  "oxlint": "^1.77.0",
@@ -42,8 +42,8 @@
42
42
  "tsdown": "^0.22.14",
43
43
  "tstyche": "^7.1.0",
44
44
  "vitest": "^4",
45
- "@systemfsoftware/effect-gherkin-spec": "4.0.2",
46
45
  "@systemfsoftware/oxlint-config": "^0.1.0",
46
+ "@systemfsoftware/effect-gherkin-spec": "4.0.2",
47
47
  "@systemfsoftware/tsconfig": "^1.3.4",
48
48
  "@systemfsoftware/vitest-config": "^0.1.0"
49
49
  },