@systemfsoftware/effect-cell-types 5.0.2 → 6.0.1

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,35 @@
1
1
  # @systemfsoftware/effect-cell-types
2
2
 
3
+ ## 6.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Peer Effect requirement advances to 4.0.0-rc.112. No API changes.
8
+
9
+ ## 6.0.0
10
+
11
+ ### Major Changes
12
+
13
+ - Cell is one sandwich: build it with `Cell.layer` and run it with `Cell.run`.
14
+
15
+ `Cell.apply` and the chained phase constructors (`Cell.read`, `Cell.decode`, `Cell.decide`, `Cell.encode`, `Cell.write`) are removed. `Cell.layer` takes one spec object — `{ read, decide, write }` rules on what the read produced, or add `decode` and `encode` together to adapt both sides of the decision.
16
+
17
+ Phase bodies that `yield*` a service publish it on the Cell's requirements channel; provide once with `Cell.provide` or `Effect.provide`. A missing provide is a compile error at the run site.
18
+
19
+ New arrows: `map`, `mapInput`, `andThen`, `zip`, `provide`, `withPolicy`. A refused decision reaches your write as the outcome value — only read, decode, and write failures fail the effect. The vocabulary drops `applier`.
20
+
21
+ `Cell.canonical`, `CanonicalCommand`, `canonicalDecide`, `PhaseFact`, and the vocabulary's `phases` list and `byKind.impure` partition are removed: the vocabulary is a const table (`module`, `ioCells`, `byKind.pure`, `composer`) and `PhaseName` is now exported. Nothing else read the walked surface; a rule that wants order reads the interpreter, not the table.
22
+
23
+ - A `Cell` description is now exactly one sandwich: read, decode, decide, encode, write. The `layers` member and the `Layer` type are deleted, `Cell.read` no longer accepts a `previous` argument, and `Cell.apply` runs that single fold; multi-layer replay no longer exists.
24
+
25
+ `Cell.layer(spec)` is new: the same description built from one object. `{ read, decide, write }` composes with identity decode and encode; `{ read, decode, decide, encode, write }` is the full form. A spec with only one of decode/encode, or a short-form write that cannot receive the decide outcome, is a compile error.
26
+
27
+ The walked vocabulary now names its composing constructor as `Cell.vocabulary.composer`, alongside `applier`.
28
+
29
+ Migrating: two layers become two descriptions applied in sequence in the calling `Effect.gen`; a write already receives the read's value as its second parameter.
30
+
31
+ - Workflow.make now requires the success channel to be a tagged union of at least two schema tagged classes sharing one family TypeId; single-variant, untagged, and unshared-brand decisions are compile errors naming the defect
32
+
3
33
  ## 5.0.2
4
34
 
5
35
  ### Patch Changes
package/README.md CHANGED
@@ -15,11 +15,14 @@ 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 — `Cell.decide` and the `DecidePhase` it
19
- demands — requires it, so a decision that skipped `make` is a compile error at the call
20
- 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 `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
21
20
  not silently collapse to that function: it resolves to a marker interface that no function
22
- can satisfy, so the mistake is a compile error with the remediation attached (below).
21
+ can satisfy, so the mistake is a compile error with the remediation attached (below). The
22
+ success channel is shaped the same way: `Workflow.make` refuses a decision channel that is
23
+ not a tagged union of at least two schema tagged classes sharing one TypeId — a single
24
+ outcome, an untagged variant, or variants with divergent family brands each resolve to a
25
+ marker interface whose property name is the remediation.
23
26
 
24
27
  ## The constructor
25
28
 
@@ -134,14 +137,18 @@ rejected, not allowed.
134
137
 
135
138
  All six violations fail `tsc`; the messages below are what `tsc` reports (verified against this package and `effect@4.0.0-rc.108`).
136
139
 
137
- | Violation | `tsc` reports | Why it is rejected |
138
- | ----------------------------------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
139
- | 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 |
140
- | 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 |
141
- | `never` decision channel | `Type '...' is not assignable to type 'UninhabitedDecision'` | a workflow that can never produce a decision can never succeed |
142
- | `never` error channel | `Type '...' is not assignable to type 'UninhabitedError'` | a workflow that cannot fail decides nothing; move it to a `*.kernel.ts` |
143
- | A bare decider handed to `Cell.decide` | `Type '(command: Cmd) => Result<Dec, Err>' is not assignable to type 'WorkflowBrand'` | only a `Workflow.make` value satisfies `DecidePhase`; a lambda that skipped `make` is not a decision a description may run |
144
- | 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 |
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 |
145
152
 
146
153
  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:
147
154
 
@@ -153,7 +160,27 @@ export interface UninhabitedDecision {
153
160
 
154
161
  export interface UninhabitedError {
155
162
  readonly __WORKFLOW_ERROR_CHANNEL_IS_NEVER__:
156
- 'this workflow cannot fail, so it decides nothing; give it an error variant or move it to a *.kernel.ts'
163
+ 'this workflow cannot fail, so it decides nothing; give it an error variant or fold the function into its owning module'
164
+ }
165
+
166
+ export interface UntaggedError {
167
+ readonly __WORKFLOW_ERROR_CHANNEL_CARRIES_NO_TAG__:
168
+ 'this error carries no _tag the consumer can dispatch on; declare it as an S.TaggedError'
169
+ }
170
+
171
+ export interface SingleVariantDecision {
172
+ readonly __WORKFLOW_DECISION_CHANNEL_HAS_ONE_VARIANT__:
173
+ 'this workflow decides one outcome, which is not a decision; add the variant it chooses between, or fold the function into its owning module'
174
+ }
175
+
176
+ export interface UntaggedDecision {
177
+ readonly __WORKFLOW_DECISION_CHANNEL_CARRIES_NO_TAG__:
178
+ 'a decision variant carries no _tag the consumer can dispatch on; declare the variants as S.TaggedClass instances'
179
+ }
180
+
181
+ export interface UnsharedTypeId {
182
+ readonly __WORKFLOW_DECISION_VARIANTS_DO_NOT_SHARE_A_TYPE_ID__:
183
+ 'the decision variants must share one TypeId — a Symbol.for family brand on each variant class'
157
184
  }
158
185
  ```
159
186
 
@@ -211,7 +238,7 @@ const decide = Workflow.make(
211
238
  // tsc: Type 'Result<Decision, unknown>' is not assignable to type 'Result<Decision, Err>'
212
239
  ```
213
240
 
214
- If the workflow genuinely cannot fail, the error channel says so — and that is a `*.kernel.ts`, not a workflow.
241
+ If the workflow genuinely cannot fail, the error channel says so — and that is a plain function inside its owning module, not a workflow.
215
242
 
216
243
  ## A wrong channel breaks the whole consumer cone
217
244
 
package/dist/index.d.ts CHANGED
@@ -2,429 +2,203 @@ import * as Effect$1 from "effect/Effect";
2
2
  import { Effect } from "effect/Effect";
3
3
  import * as Result$1 from "effect/Result";
4
4
  import { Result } from "effect/Result";
5
- import * as Schema$1 from "effect/Schema";
6
5
  import { Schema } from "effect";
6
+ import { Kind, TypeLambda } from "effect/HKT";
7
+ import { Layer } from "effect/Layer";
8
+ import * as Schema$1 from "effect/Schema";
9
+ //#region src/Facts.d.ts
10
+ declare const DESCRIPTION_MODULE: '@systemfsoftware/effect-cell-types';
11
+ declare const IO_CELLS: {
12
+ readonly cells: readonly ['store', 'adapter'];
13
+ readonly sources: readonly ['effect/Clock', 'effect/System'];
14
+ };
15
+ type IoCellClassification = typeof IO_CELLS;
16
+ type PhaseName = 'read' | 'decode' | 'decide' | 'encode' | 'write';
17
+ declare namespace Policy_d_exports {
18
+ export { Policy };
19
+ }
20
+ type Policy<A, E, R> = (self: Effect<A, E, R>) => Effect<A, E, R>;
7
21
  declare namespace Workflow_d_exports {
8
- export { Inhabited, UninhabitedDecision, UninhabitedError, UntaggedError, Workflow, WorkflowBrand, make };
22
+ export { Inhabited, SingleVariantDecision, UninhabitedDecision, UninhabitedError, UnsharedTypeId, UntaggedDecision, UntaggedError, Workflow, WorkflowBrand, make };
9
23
  }
10
- /**
11
- * The nominal brand a workflow carries. `Workflow.make` is the only door that applies it,
12
- * and every surface that runs a decision — `Cell.decide`, and through it `DecideNode.run`
13
- * via the `DecidePhase` conjunct — demands it, so a bare decider that skipped `make` is
14
- * refused by the compiler at the call site that would have run it.
15
- *
16
- * The brand is phantom — a readonly TypeId-keyed field that no runtime property ever
17
- * backs: `assertWorkflow` narrows the same function value without touching it. The
18
- * `Symbol.for` key follows the repo's branded-class idiom (the instance brands in
19
- * `restart-decision.workflow.ts`, `hook-verdict.workflow.ts` and `survivors.workflow.ts`)
20
- * so it is stable across realms; this is the type-level brand that subsumes those
21
- * instance brands on the workflow shapes.
22
- */
23
24
  declare const WorkflowTypeId: unique symbol;
24
25
  type WorkflowTypeId = typeof WorkflowTypeId;
25
- /** The phantom property `Workflow<C,D,E>` and `Cell.DecidePhase<P>` carry. */
26
26
  interface WorkflowBrand {
27
27
  readonly [WorkflowTypeId]: WorkflowTypeId;
28
28
  }
29
- /**
30
- * Marker a workflow resolves to when its decision channel is `never`. The property type is the
31
- * fix, so the compiler diagnostic names it.
32
- */
33
29
  interface UninhabitedDecision {
34
30
  readonly __WORKFLOW_DECISION_CHANNEL_IS_NEVER__: 'this workflow can never succeed; give it a decision variant it can return';
35
31
  }
36
- /** Marker a workflow resolves to when its error channel is `never`. */
37
32
  interface UninhabitedError {
38
- readonly __WORKFLOW_ERROR_CHANNEL_IS_NEVER__: 'this workflow cannot fail, so it decides nothing; give it an error variant or move it to a *.kernel.ts';
33
+ 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';
39
34
  }
40
- /** Marker a workflow resolves to when its error channel carries no tag to dispatch on. */
41
35
  interface UntaggedError {
42
36
  readonly __WORKFLOW_ERROR_CHANNEL_CARRIES_NO_TAG__: 'this error carries no _tag the consumer can dispatch on; declare it as an S.TaggedError';
43
37
  }
44
- /**
45
- * A decider whose channels are both inhabited, or the marker naming which channel is not.
46
- *
47
- * `[T] extends [never]` rather than `T extends never`: the tuple wrap stops distribution, without
48
- * which a `never` channel satisfies the conditional vacuously and is never caught.
49
- *
50
- * The inhabited branch carries the nominal brand: `Workflow.make` is the only constructor that
51
- * applies it, so a bare function annotated `Workflow<C, D, E>` is refused wherever the brand
52
- * is demanded — which is exactly where a decision gets run.
53
- */
38
+ interface SingleVariantDecision {
39
+ 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';
40
+ }
41
+ interface UntaggedDecision {
42
+ 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';
43
+ }
44
+ interface UnsharedTypeId {
45
+ 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';
46
+ }
47
+ type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
48
+ type AtLeastTwoDistinct<T, U = T> = U extends unknown ? [T] extends [U] ? false : true : never;
49
+ type TaggedMembers<D> = D extends unknown ? '_tag' extends keyof D ? [D['_tag']] extends [string] ? true : false : false : never;
50
+ type SharedTypeId<D, I = UnionToIntersection<D>> = [keyof { [K in keyof I as I[K] extends K ? (D extends { readonly [P in K]: K; } ? K : never) : never]: 0; }] extends [never] ? UnsharedTypeId : unknown;
51
+ type DecisionShape<D> = [unknown] extends [D] ? unknown : AtLeastTwoDistinct<D> extends false ? SingleVariantDecision : boolean extends TaggedMembers<D> ? UntaggedDecision : SharedTypeId<D>;
54
52
  type Workflow<Command, Decision, DecisionError> = [Decision] extends [never] ? UninhabitedDecision : [DecisionError] extends [never] ? UninhabitedError : ((command: Command) => Result<Decision, DecisionError>) & WorkflowBrand;
55
- /**
56
- * `unknown` when the error channel carries a tag a consumer can dispatch on, the {@link UntaggedError}
57
- * marker otherwise. Two steps, both load-bearing: `'_tag' extends keyof E` asks whether the key is
58
- * there, and `[E['_tag']] extends [string]` asks whether what it holds is dispatchable. Key presence
59
- * alone admits `_tag: number`, `_tag?: string` and a `_tag()` method — none of which `Match.tag` can
60
- * dispatch on, which is exactly what the marker claims to refuse.
61
- *
62
- * Both steps read the tag through `keyof` and an indexed access rather than declaring a `_tag`
63
- * member, which is why the erased `Tagged` interface cannot come back: stating the requirement as a
64
- * shape would write the very member this repo forbids.
65
- */
66
53
  type DispatchableTag<E> = '_tag' extends keyof E ? [E['_tag']] extends [string] ? unknown : UntaggedError : UntaggedError;
67
- /**
68
- * `unknown` when both channels are inhabited and the error carries a dispatchable tag, so the
69
- * intersection in {@link make} collapses to the plain `Result` and neither inference nor the
70
- * authoring surface changes. Otherwise the marker the author must satisfy, which they cannot, which
71
- * is the point.
72
- */
73
- type Inhabited<Decision, DecisionError> = [Decision] extends [never] ? UninhabitedDecision : [DecisionError] extends [never] ? UninhabitedError : DispatchableTag<DecisionError>;
74
- /**
75
- * Builds a workflow from the command's schema class and a decider over that class's
76
- * instance type, refusing an uninhabited or untagged channel at this call rather than at
77
- * whoever first calls the result — which for a workflow nothing calls yet is never.
78
- *
79
- * The command is constrained on the **value**, not on a type parameter inferred from the
80
- * decider's parameter. That is the whole mechanism. Any constraint on such a parameter is a
81
- * structural predicate, and TypeScript cannot say "this type came from a class declaration"
82
- * — so a marker placed there is a property, every property is declarable, and
83
- * `interface Fake extends Marker {}` satisfies it. A declared type produces no value, so it
84
- * cannot reach an argument position at all: there is no marker to smuggle because there is
85
- * no marker.
86
- *
87
- * The three parameters mirror `Schema.Class`'s own bound exactly, and that is load-bearing.
88
- * `Class<Self, S, Inherited>` places `S` in both covariant (`S["Type"]`) and contravariant
89
- * (`S["fields"]`) positions, so it is invariant in `S`: every *fixed* spelling —
90
- * `Class<unknown, Struct<Struct.Fields>, unknown>` and its variants — rejects real command
91
- * classes. Generic over `S` accepts them and still refuses a `Struct`, which lacks
92
- * `identifier` and `extend`. `Class<any, any, any>` also works and is banned here.
93
- *
94
- * `Schema.TaggedClass` returns this same `Class` interface, so one constraint covers both
95
- * factories with no union. The import is type-only: this package gains no runtime dependency
96
- * on Effect Schema, and `make` stays the identity function it always was.
97
- *
98
- * The markers ride the parameter function's return type, not the parameter as `Workflow<C, D, E>`:
99
- * a conditional type in parameter position resolves `D` and `E` to `unknown` and the markers become
100
- * unreachable. On the return type both still infer from the `Result` conjunct while the marker
101
- * conjunct is what an uninhabited channel fails to satisfy.
102
- *
103
- * `E` carries no constraint on purpose. Constraining it gives inference a fallback: where `E` would
104
- * infer as `never` TypeScript substitutes the constraint instead, the conditional takes its
105
- * inhabited branch, and a `never` channel passes. The tagged requirement therefore lives in
106
- * {@link Inhabited}, where nothing can stand in for `never`.
107
- *
108
- * The narrowing goes through an assertion signature rather than an `as` cast: every narrowing
109
- * assertion trips `typescript(no-unsafe-type-assertion)`, and a suppression comment would hide the
110
- * one place this file could lie. It is sound rather than merely permitted — with both channels
111
- * inhabited `Workflow<Self, D, E>` is `(command: Self) => Result<D, E>` carrying the
112
- * {@link WorkflowBrand} conjunct, and otherwise the return type is a marker with no call
113
- * signature, so the value handed back is unobservable through it. The brand is applied here and
114
- * nowhere else: the assertion adds no runtime property, yet a value that did not pass through this
115
- * door fails the conjunct wherever a decision is run.
116
- */
54
+ type Inhabited<Decision, DecisionError> = [Decision] extends [never] ? UninhabitedDecision : [DecisionError] extends [never] ? UninhabitedError : DecisionShape<Decision> & DispatchableTag<DecisionError>;
117
55
  declare const make: <Self, S extends Schema$1.Constraint & {
118
56
  readonly fields: Schema$1.Struct.Fields;
119
57
  }, Inherited, D, E>(_command: Schema$1.Class<Self, S, Inherited>, decide: (command: Self) => Result<D, E> & Inhabited<D, E>) => Workflow<Self, D, E>;
120
- //#endregion
121
- //#region src/CanonicalDecide.workflow.d.ts
122
- declare const CanonicalCommand_base: Schema$1.Class<CanonicalCommand, Schema$1.TaggedStruct<"CanonicalCommand", {}>, {}>;
123
- /**
124
- * The canonical command. `Workflow.make` constrains its first argument to a real
125
- * schema class, so the canonical description needs one too — it carries no fields
126
- * because the canonical's phases do nothing, and its only job is to be a genuine
127
- * command value rather than a shape asserted into place.
128
- *
129
- * It is declared here rather than in a `*.schema.ts` because this is the owning
130
- * single-segment `<stem>.workflow.ts`, which `schema-declaration-location` admits.
131
- */
132
- declare class CanonicalCommand extends CanonicalCommand_base {}
133
58
  declare namespace Cell_d_exports {
134
- export { Convention, DESCRIPTION_MODULE, DecideDone, DecideNode, DecidePhase, DecodeDone, DecodeNode, DecodePhase, Description, EncodeDone, EncodeNode, EncodePhase, IO_CELLS, IoCellClassification, Layer, Phase, PhaseFact, Phases, ReadDone, ReadNode, ReadPhase, Vocabulary, WriteDone, WriteNode, WritePhase, apply, canonical, decide, decode, encode, read, vocabulary, write };
59
+ export { Cell, CellTypeId, DESCRIPTION_MODULE, IO_CELLS, IoCellClassification, Kind$1 as Kind, PhaseName, Run, TypeLambda$1 as TypeLambda, Vocabulary, andThen, layer, map, mapInput, provide, run, vocabulary, withPolicy, zip };
135
60
  }
136
61
  /**
137
- * The type bag. Every phase's input and output type travels in one record so that a
138
- * stage's own type arguments are identical across stages, which leaves the sentence
139
- * member as the only difference a diagnostic can report. Measured: with the payload
140
- * carried per-stage instead, the compiler reports the mismatch at the argument (TS2345)
141
- * and the sentence arrives only as a type argument; with the payload in one bag it
142
- * reports the missing member (TS2741) and the sentence is that member's name.
62
+ * The nominal brand every `Cell` carries. `Cell.layer` is the only door that applies it.
143
63
  */
144
- interface Phases {
145
- readonly command: unknown;
146
- readonly raw: unknown;
147
- readonly decoded: unknown;
148
- readonly decision: unknown;
149
- readonly decisionError: unknown;
150
- readonly output: unknown;
151
- readonly response: unknown;
152
- readonly decodeError: unknown;
153
- readonly readError: unknown;
154
- readonly writeError: unknown;
155
- }
64
+ declare const CellTypeId: unique symbol;
156
65
  /**
157
- * A read gathers what the decision needs, and may gather a product across its interior;
158
- * that interior is not type-visible, so no I/O count is claimed or enforced here. A step that
159
- * mutates in order to report — bumping a counter and returning the resulting rate — is one
160
- * such product, and belongs here rather than in a layer of its own.
161
- *
162
- * The context channel is pinned `never`: a phase requires nothing. Services are resolved by
163
- * whoever builds the description and handed to the phase as ordinary parameters, which is
164
- * the same edge that already gathers the read's inputs. The alternative — a `readContext`
165
- * member on the bag — let an author write `never` for a body that reaches for a service, and
166
- * nothing checked the claim: under a stage generic over `Phases` the compiler cannot see the
167
- * lambda's requirement at all, so the description compiled and the missing service surfaced
168
- * only where it was finally applied, or nowhere. Pinning it makes the lie unrepresentable
169
- * instead of merely discouraged, and leaves `apply`'s derived `R` honestly `never`.
66
+ * The nominal brand type of {@link CellTypeId}.
170
67
  */
171
- type ReadPhase<P extends Phases> = (command: P['command']) => Effect$1.Effect<P['raw'], P['readError'], never>;
172
- /** Validation. Its `Left` is fatal: it reaches the derived error channel and no write runs. */
173
- type DecodePhase<P extends Phases> = (raw: P['raw']) => Result$1.Result<P['decoded'], P['decodeError']>;
68
+ type CellTypeId = typeof CellTypeId;
174
69
  /**
175
- * The decision. Its `Left` is an outcome, not a fault: both branches travel on to the write.
176
- *
177
- * The {@link WorkflowBrand} conjunct makes this the one surface a decision must cross
178
- * branded: only a `Workflow.make` value satisfies it, so a bare lambda handed here is
179
- * refused by the compiler with the brand conjunct named in the diagnostic. The `run` on
180
- * a `DecideNode` inherits the conjunct through this type, which keeps the interpreter's
181
- * fold sound — a description can only carry decisions that came through `make`.
70
+ * A Cell is one sandwich: a `read` that gathers, a `decide` that refuses or rules, and a
71
+ * `write` that acts — compiled into a single function from command to response. The `E`
72
+ * channel carries the infrastructure refusals; the `R` channel carries the services the
73
+ * phases `yield*`, provided once by the program's composition root.
182
74
  */
183
- type DecidePhase<P extends Phases> = ((decoded: P['decoded']) => Result$1.Result<P['decision'], P['decisionError']>) & WorkflowBrand;
184
- /** Shapes what the write consumes. Total, so it receives both branches of the decision. */
185
- type EncodePhase<P extends Phases> = (outcome: Result$1.Result<P['decision'], P['decisionError']>) => P['output'];
75
+ interface Cell<in I, out A, out E = never, out R = never> {
76
+ readonly [CellTypeId]: CellTypeId;
77
+ readonly run: (input: I) => Effect$1.Effect<A, E, R>;
78
+ }
186
79
  /**
187
- * The write. It receives the encoded `output` and, as a second argument, the `raw` its own
188
- * layer's read gathered.
189
- *
190
- * `raw` is there because a write is frequently the point that persists or reports what the
191
- * read found, while the decision in between deliberately narrows to what it needed. Without
192
- * this argument such a write has no channel for it and the layer smuggles the value through
193
- * a closure — a `let` beside the description, assigned in the read and consulted in the
194
- * write, which then needs a runtime guard for a value the fold has already produced. The
195
- * argument is second, and a write that does not want it declares one parameter: a unary
196
- * function satisfies this type, so every write written before it existed is unchanged.
80
+ * The type lambda for {@link Cell}, admitting Cell to `Kind` positions.
197
81
  */
198
- type WritePhase<P extends Phases> = (output: P['output'], raw: P['raw']) => Effect$1.Effect<P['response'], P['writeError'], never>;
82
+ interface TypeLambda$1 extends TypeLambda {
83
+ readonly type: Cell<this['In'], this['Target'], this['Out2'], this['Out1']>;
84
+ }
199
85
  /**
200
- * The invocation shape a folding consumer must use to call a phase's `run`:
201
- * - `'effect'` — `run` returns an `Effect`; yield it. (read, write)
202
- * - `'either-fail'` — `run` returns a `Result` whose `Failure` is fatal; fail on `Failure`. (decode)
203
- * - `'either-pass'` — `run` returns a `Result` that travels forward whole. (decide)
204
- * - `'total'` — `run` is a plain total function; call it directly. (encode)
205
- *
206
- * This is structural data on the record, not one of the five axes: it lets an executing
207
- * consumer fold the description without knowing which phase it is looking at. The
208
- * interpreter's switch over `convention` is exhaustively defaulted, so a phase with an
209
- * invocation shape this module does not know fails at compile time at one named location.
86
+ * A fully-applied {@link Kind} for Cell: `Kind<I, E, R, A>` is `Cell<I, A, E, R>`.
210
87
  */
211
- type Convention = 'effect' | 'either-fail' | 'either-pass' | 'total';
88
+ type Kind$1<I, E, R, A> = Kind<TypeLambda$1, I, E, R, A>;
212
89
  /**
213
- * One phase record: `name` (the phase name, as data), `kind` (purity), `convention`
214
- * (the invocation shape), and the phase's `run`. Each phase type has its own record
215
- * interface so `run` keeps its exact signature. Nothing here discriminates on `name`:
216
- * the interpreter dispatches on `convention`, and other consumers read `name` as data.
217
- * A hand-written `_tag` is deliberately absent — a manual `_tag` member is forbidden
218
- * here (the schema rule prescribes `TaggedStruct`, which cannot describe a function
219
- * member), and an ordered sequence of phase records is not a `Match`-style tagged union.
90
+ * The function shape a Cell publishes, read off the Cell type itself. Use it to type a
91
+ * capability parameter or a callback that hands a Cell's run to a shell:
92
+ * `Run<I, A, E, R>` is `Cell<I, A, E, R>['run']`.
220
93
  */
221
- interface ReadNode<P extends Phases> {
222
- readonly name: 'read';
223
- readonly kind: 'impure';
224
- readonly convention: 'effect';
225
- readonly run: ReadPhase<P>;
94
+ type Run<I, A, E, R> = Cell<I, A, E, R>['run'];
95
+ interface LayerCore<I, Raw, RE, RR, Dec, DE, Resp, WE, WR> {
96
+ readonly read: (command: I) => Effect$1.Effect<Raw, RE, RR>;
97
+ readonly decide: ((decoded: Raw) => Result$1.Result<Dec, DE>) & WorkflowBrand;
98
+ readonly write: (output: Result$1.Result<Dec, DE>, raw: Raw) => Effect$1.Effect<Resp, WE, WR>;
226
99
  }
227
- interface DecodeNode<P extends Phases> {
228
- readonly name: 'decode';
229
- readonly kind: 'pure';
230
- readonly convention: 'either-fail';
231
- readonly run: DecodePhase<P>;
100
+ interface LayerShortSpec<I, Raw, RE, RR, Dec, DE, Resp, WE, WR> extends LayerCore<I, Raw, RE, RR, Dec, DE, Resp, WE, WR> {
101
+ readonly decode?: never;
102
+ readonly encode?: never;
232
103
  }
233
- interface DecideNode<P extends Phases> {
234
- readonly name: 'decide';
235
- readonly kind: 'pure';
236
- readonly convention: 'either-pass';
237
- readonly run: DecidePhase<P>;
104
+ 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'> {
105
+ readonly decode: (raw: Raw) => Result$1.Result<Dcd, DecE>;
106
+ readonly decide: ((decoded: Dcd) => Result$1.Result<Dec, DE>) & WorkflowBrand;
107
+ readonly encode: (outcome: Result$1.Result<Dec, DE>) => Out;
108
+ readonly write: (output: Out, raw: Raw) => Effect$1.Effect<Resp, WE, WR>;
238
109
  }
239
- interface EncodeNode<P extends Phases> {
240
- readonly name: 'encode';
241
- readonly kind: 'pure';
242
- readonly convention: 'total';
243
- readonly run: EncodePhase<P>;
244
- }
245
- interface WriteNode<P extends Phases> {
246
- readonly name: 'write';
247
- readonly kind: 'impure';
248
- readonly convention: 'effect';
249
- readonly run: WritePhase<P>;
250
- }
251
- type Phase<P extends Phases> = ReadNode<P> | DecodeNode<P> | DecideNode<P> | EncodeNode<P> | WriteNode<P>;
252
- /**
253
- * One impure/pure layer: its phase records in intra-layer execution order. The order is
254
- * data — the interpreter folds this array and runs each record as its `convention`
255
- * says, holding no phase sequence of its own. The stage brands are what make a legal
256
- * description well-ordered at build time; the value's declared order is its execution
257
- * order.
258
- */
259
- interface Layer<P extends Phases> {
260
- readonly phases: readonly Phase<P>[];
261
- }
262
- /** The description package's own module name — what an import edge would match. */
263
- declare const DESCRIPTION_MODULE: '@systemfsoftware/effect-cell-types';
264
110
  /**
265
- * The I/O-cell classification: the cells whose calls are I/O, plus the non-cell module
266
- * sources whose calls are I/O. Written once here; a consumer folds it off the value.
111
+ * Builds a Cell from one sandwich.
112
+ *
113
+ * Short form — `read` produces the value `decide` rules on, and the decide outcome is what
114
+ * `write` receives:
115
+ *
116
+ * ```ts
117
+ * import { Cell, Workflow } from '@systemfsoftware/effect-cell-types'
118
+ * import { Effect, Result } from 'effect'
119
+ *
120
+ * declare const decideAdmission: Workflow<CliArgs, Verdict, Refusal>
121
+ * declare class CliArgs { readonly target: string }
122
+ * declare class Verdict { readonly ok: boolean }
123
+ * declare class Refusal { readonly _tag: 'Refused' }
124
+ *
125
+ * const cell = Cell.layer({
126
+ * read: (args: CliArgs) => Effect.succeed(args),
127
+ * decide: decideAdmission,
128
+ * write: (outcome: Result.Result<Verdict, Refusal>, raw: CliArgs) => Effect.void,
129
+ * })
130
+ * ```
131
+ *
132
+ * Long form — `decode` and `encode` adapt each side of `decide`; both are required together,
133
+ * and a spec carrying one without the other fails inference.
267
134
  */
268
- declare const IO_CELLS: {
269
- readonly cells: readonly ['store', 'adapter'];
270
- readonly sources: readonly ['effect/Clock', 'effect/System'];
135
+ 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>;
136
+ 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>;
137
+ declare const run: {
138
+ <I>(input: I): <A, E, R>(self: Cell<I, A, E, R>) => Effect$1.Effect<A, E, R>;
139
+ <I, A, E, R>(self: Cell<I, A, E, R>, input: I): Effect$1.Effect<A, E, R>;
271
140
  };
272
141
  /**
273
- * Derived from the value rather than restated beside it: a hand-written twin is a second
274
- * declaration of axis 5, and the two drift the moment a cell is reclassified in only one.
142
+ * Transforms the Cell's response.
275
143
  */
276
- type IoCellClassification = typeof IO_CELLS;
144
+ declare const map: {
145
+ <A, B>(f: (a: A) => B): <I, E, R>(self: Cell<I, A, E, R>) => Cell<I, B, E, R>;
146
+ <I, A, E, R, B>(self: Cell<I, A, E, R>, f: (a: A) => B): Cell<I, B, E, R>;
147
+ };
277
148
  /**
278
- * The description root, carried by every stage. A consumer folds a stage value to
279
- * recover the description's whole vocabulary: the phase names and kinds on each
280
- * record, the intra-layer order in each `phases` array, the package's own module
281
- * name, and the I/O-cell classification. Nothing about the shape of a legal
282
- * description is written down anywhere else.
149
+ * Transforms the Cell's input.
283
150
  */
284
- interface Description<P extends Phases> {
285
- readonly module: typeof DESCRIPTION_MODULE;
286
- readonly ioCells: IoCellClassification;
287
- readonly layers: readonly Layer<P>[];
288
- }
151
+ declare const mapInput: {
152
+ <I0, I>(f: (input: I0) => I): <A, E, R>(self: Cell<I, A, E, R>) => Cell<I0, A, E, R>;
153
+ <I0, I, A, E, R>(self: Cell<I, A, E, R>, f: (input: I0) => I): Cell<I0, A, E, R>;
154
+ };
289
155
  /**
290
- * The stages are siblings, never a hierarchy. Each carries exactly the sentence naming the
291
- * call that must come next, and none extends another. Measured: under a hierarchy a later
292
- * stage is assignable to an earlier parameter, so an inversion — decoding what was already
293
- * decided — compiles. As siblings both the forward skip and the backward inversion are
294
- * rejected, each diagnostic naming the sentence it is missing.
295
- *
296
- * The carrier is deliberately the same type on every stage — `Description` — so the
297
- * compiler reports the argument rather than the member only when the carrier itself is
298
- * wrong, and the sentence stays the name a diagnostic reports.
156
+ * Feeds this Cell's response to the next Cell as its input. The error and service channels
157
+ * union.
299
158
  */
300
- interface ReadDone<P extends Phases> extends Description<P> {
301
- readonly 'call read(command) before decode(raw)': true;
302
- }
303
- interface DecodeDone<P extends Phases> extends Description<P> {
304
- readonly 'call decode(raw) before decide(decoded)': true;
305
- }
306
- interface DecideDone<P extends Phases> extends Description<P> {
307
- readonly 'call decide(decoded) before encode(decision)': true;
308
- }
309
- interface EncodeDone<P extends Phases> extends Description<P> {
310
- readonly 'call encode(decision) before write(output)': true;
311
- }
312
- /** Terminal. A description is applied from here, and a further layer opens from here. */
313
- interface WriteDone<P extends Phases> extends Description<P> {
314
- readonly 'call write(output) before applying the description': true;
315
- }
159
+ declare const andThen: {
160
+ <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>;
161
+ <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>;
162
+ };
316
163
  /**
317
- * Opens a layer. Passing a prior `WriteDone` opens a second layer over the same bag, so a
318
- * call site whose real order writes before it can classify is one description carrying two
319
- * layers rather than two descriptions composed by hand.
320
- *
321
- * This one is not dual: it starts the chain, so on the opening layer it has no `self` to
322
- * receive. Every phase after it is dual, which is what lets a description be written in the
323
- * order it runs.
164
+ * Runs both Cells against the same input and tuples the responses. Fails fast: when one
165
+ * side refuses, the other's write never runs.
324
166
  */
325
- declare const read: <P extends Phases>(run: ReadPhase<P>, previous?: WriteDone<P>) => ReadDone<P>;
167
+ declare const zip: {
168
+ <I, B, E2, R2>(that: Cell<I, B, E2, R2>): <A, E, R>(self: Cell<I, A, E, R>) => Cell<I, readonly [A, B], E | E2, R | R2>;
169
+ <I, A, E, R, B, E2, R2>(self: Cell<I, A, E, R>, that: Cell<I, B, E2, R2>): Cell<I, readonly [A, B], E | E2, R | R2>;
170
+ };
326
171
  /**
327
- * The chaining phases are dual, data-last overload declared first. Nesting the constructors
328
- * reads innermost-first backwards from the order the phases run — which defeats the point of
329
- * a type that exists to make that order legible. In `pipe` the call site reads in phase order,
330
- * and the sentence still arrives as a missing member through it.
331
- *
332
- * A `Do`-notation scope binding each phase's result for later phases to read was measured as
333
- * an alternative: the sentence survives it, even with the scope varying per stage, because an
334
- * absent member is reported before type arguments are compared. It was not adopted, because a
335
- * scope the interpreter folds over is type-erased, and reading a phase back out of it needs an
336
- * assertion this design does not.
172
+ * Provides a Layer to the Cell, eliminating the services the layer builds from `R`. This is
173
+ * the one composition-root elimination; the resulting Cell still demands the layer's input
174
+ * services. A missing provide is a compile error at the run site.
337
175
  */
338
- declare const decode: {
339
- <P extends Phases>(run: DecodePhase<P>): (previous: ReadDone<P>) => DecodeDone<P>;
340
- <P extends Phases>(previous: ReadDone<P>, run: DecodePhase<P>): DecodeDone<P>;
341
- };
342
- declare const decide: {
343
- <P extends Phases>(run: DecidePhase<P>): (previous: DecodeDone<P>) => DecideDone<P>;
344
- <P extends Phases>(previous: DecodeDone<P>, run: DecidePhase<P>): DecideDone<P>;
345
- };
346
- declare const encode: {
347
- <P extends Phases>(run: EncodePhase<P>): (previous: DecideDone<P>) => EncodeDone<P>;
348
- <P extends Phases>(previous: DecideDone<P>, run: EncodePhase<P>): EncodeDone<P>;
349
- };
350
- declare const write: {
351
- <P extends Phases>(run: WritePhase<P>): (previous: EncodeDone<P>) => WriteDone<P>;
352
- <P extends Phases>(previous: EncodeDone<P>, run: WritePhase<P>): WriteDone<P>;
176
+ declare const provide: {
177
+ <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>>;
178
+ <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>>;
353
179
  };
354
180
  /**
355
- * Applies a description. The return type is deliberately not annotated: `gen` accumulates
356
- * `E` and `R` from the union of what is actually yielded, so an over-claimed channel is
357
- * unrepresentable rather than merely discouraged. Annotating it here would let this module
358
- * promise a failure that no phase can produce.
359
- *
360
- * The parameter keeps the terminal `WriteDone<P>` brand. It is not decoration: it is what
361
- * makes applying a half-built chain — a `ReadDone`, say — a compile error rather than a
362
- * runtime death, which `test-types/Cell.tst.ts` pins. The brand does not constrain the
363
- * phases array's order (a literal satisfies it in any order, which is why the interpreter
364
- * reads the order off the value), but it does constrain chain completion, and that is a
365
- * guarantee worth the narrower parameter.
366
- *
367
- * `forEach` takes no concurrency option here, so the layers run in declared order and the
368
- * sequence is structural rather than something a caller could pass differently; the
369
- * description's response is the last layer's. No scope is opened and interruptibility is
370
- * untouched, so a `Scope.Scope` a phase requires reaches the caller as part of the derived `R`.
181
+ * Wraps the Cell's run in a `Policy` retry, timeout, and their kin preserving every
182
+ * channel.
371
183
  */
372
- declare const apply: <P extends Phases>(description: WriteDone<P>, command: P['command']) => Effect$1.Effect<P["response"], P["decodeError"] | P["readError"] | P["writeError"], never>;
373
- /** One phase's vocabulary entry: what it is called, its purity, its invocation shape. */
374
- interface PhaseFact {
375
- readonly name: Phase<Phases>['name'];
376
- readonly kind: Phase<Phases>['kind'];
377
- readonly convention: Convention;
378
- }
184
+ declare const withPolicy: {
185
+ <A, E, R>(policy: Policy<A, E, R>): <I>(self: Cell<I, A, E, R>) => Cell<I, A, E, R>;
186
+ <I, A, E, R>(self: Cell<I, A, E, R>, policy: Policy<A, E, R>): Cell<I, A, E, R>;
187
+ };
379
188
  /**
380
- * The five axes as data, for a consumer that has no description of its own to fold.
381
- *
382
- * `byKind` groups the walked phase names by their purity. It is here rather than left to
383
- * each consumer because which phases are pure is this module's own fact, and a consumer
384
- * that reconstructs it has to pick a proxy — inferring purity from the invocation shape,
385
- * say — which is a different axis and silently disagrees the moment a pure phase is given
386
- * an effectful shape or an impure one is not.
189
+ * The facts the lint plugin judges a spec body by, as a const table. The order the
190
+ * interpreter runs is the text of {@link layerRunner}; the table states only what a
191
+ * rule cannot read off a type: which phases are pure, and what counts as I/O.
387
192
  */
388
193
  interface Vocabulary {
389
194
  readonly module: typeof DESCRIPTION_MODULE;
390
195
  readonly ioCells: IoCellClassification;
391
- readonly phases: readonly PhaseFact[];
392
- readonly byKind: Readonly<Record<PhaseFact['kind'], readonly PhaseFact['name'][]>>;
393
- /**
394
- * The export that runs a finished description. A consumer deciding which calls on this module
395
- * belong to a description needs the phases *and* the applier; without it the applier is the one
396
- * name it has to write down for itself, and one restated name is enough to drift.
397
- */
398
- readonly applier: 'apply';
399
- }
400
- /**
401
- * The bag the canonical description is built with. It is `Phases` with one member
402
- * pinned: `decoded` is the canonical command class, because `canonicalDecide` is a
403
- * decider over that class and a decider's parameter is contravariant — a phase typed
404
- * `(decoded: unknown) => …` would demand that `unknown` be assignable to the command,
405
- * which it is not. Every other member stays `unknown`, so nothing else narrows.
406
- */
407
- interface CanonicalPhases extends Phases {
408
- readonly decoded: CanonicalCommand;
196
+ readonly byKind: {
197
+ readonly pure: readonly PhaseName[];
198
+ };
199
+ readonly composer: 'layer';
409
200
  }
410
- /**
411
- * A canonical description, built through the public constructors with phases that do
412
- * nothing. It is exported so a consumer — a generator, a lint rule, a documenter — can
413
- * obtain a real branded description without replaying the constructor chain: spread it
414
- * and substitute its phase records' `run`s. The records it carries are the same literals
415
- * the constructors write for real call sites, so the vocabulary below cannot drift from
416
- * them.
417
- *
418
- * Its order is not a choice this module makes. The stage brands admit exactly one chain, so
419
- * any other sequence fails to typecheck here — which is what keeps the derived order
420
- * non-circular: it is read off a value, and the value's shape is enforced by the types.
421
- */
422
- declare const canonical: WriteDone<CanonicalPhases>;
423
201
  declare const vocabulary: Vocabulary;
424
- declare namespace Policy_d_exports {
425
- export { Policy };
426
- }
427
- type Policy<A, E, R> = (self: Effect<A, E, R>) => Effect<A, E, R>;
428
202
  declare namespace Wire_d_exports {
429
203
  export { AnyMinted, Fields, Mark, Minted, MintedField, mint, wire };
430
204
  }
package/dist/index.mjs CHANGED
@@ -1,311 +1,107 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
- import * as Arr from "effect/Array";
3
2
  import * as Effect$1 from "effect/Effect";
4
3
  import { dual } from "effect/Function";
5
- import * as Option from "effect/Option";
6
4
  import * as Result$1 from "effect/Result";
7
- import * as S from "effect/Schema";
8
5
  import { Schema } from "effect";
9
- S.TaggedError()("CanonicalDecideError", {});
6
+ //#region src/Facts.ts
7
+ const DESCRIPTION_MODULE = "@systemfsoftware/effect-cell-types";
8
+ const IO_CELLS = {
9
+ cells: ["store", "adapter"],
10
+ sources: ["effect/Clock", "effect/System"]
11
+ };
10
12
  //#endregion
11
13
  //#region src/Workflow.ts
12
- var Workflow_exports = /* @__PURE__ */ __exportAll({ make: () => make });
13
- /**
14
- * Builds a workflow from the command's schema class and a decider over that class's
15
- * instance type, refusing an uninhabited or untagged channel at this call rather than at
16
- * whoever first calls the result — which for a workflow nothing calls yet is never.
17
- *
18
- * The command is constrained on the **value**, not on a type parameter inferred from the
19
- * decider's parameter. That is the whole mechanism. Any constraint on such a parameter is a
20
- * structural predicate, and TypeScript cannot say "this type came from a class declaration"
21
- * — so a marker placed there is a property, every property is declarable, and
22
- * `interface Fake extends Marker {}` satisfies it. A declared type produces no value, so it
23
- * cannot reach an argument position at all: there is no marker to smuggle because there is
24
- * no marker.
25
- *
26
- * The three parameters mirror `Schema.Class`'s own bound exactly, and that is load-bearing.
27
- * `Class<Self, S, Inherited>` places `S` in both covariant (`S["Type"]`) and contravariant
28
- * (`S["fields"]`) positions, so it is invariant in `S`: every *fixed* spelling —
29
- * `Class<unknown, Struct<Struct.Fields>, unknown>` and its variants — rejects real command
30
- * classes. Generic over `S` accepts them and still refuses a `Struct`, which lacks
31
- * `identifier` and `extend`. `Class<any, any, any>` also works and is banned here.
32
- *
33
- * `Schema.TaggedClass` returns this same `Class` interface, so one constraint covers both
34
- * factories with no union. The import is type-only: this package gains no runtime dependency
35
- * on Effect Schema, and `make` stays the identity function it always was.
36
- *
37
- * The markers ride the parameter function's return type, not the parameter as `Workflow<C, D, E>`:
38
- * a conditional type in parameter position resolves `D` and `E` to `unknown` and the markers become
39
- * unreachable. On the return type both still infer from the `Result` conjunct while the marker
40
- * conjunct is what an uninhabited channel fails to satisfy.
41
- *
42
- * `E` carries no constraint on purpose. Constraining it gives inference a fallback: where `E` would
43
- * infer as `never` TypeScript substitutes the constraint instead, the conditional takes its
44
- * inhabited branch, and a `never` channel passes. The tagged requirement therefore lives in
45
- * {@link Inhabited}, where nothing can stand in for `never`.
46
- *
47
- * The narrowing goes through an assertion signature rather than an `as` cast: every narrowing
48
- * assertion trips `typescript(no-unsafe-type-assertion)`, and a suppression comment would hide the
49
- * one place this file could lie. It is sound rather than merely permitted — with both channels
50
- * inhabited `Workflow<Self, D, E>` is `(command: Self) => Result<D, E>` carrying the
51
- * {@link WorkflowBrand} conjunct, and otherwise the return type is a marker with no call
52
- * signature, so the value handed back is unobservable through it. The brand is applied here and
53
- * nowhere else: the assertion adds no runtime property, yet a value that did not pass through this
54
- * door fails the conjunct wherever a decision is run.
55
- */
56
- const make = (_command, decide) => {
14
+ var Workflow_exports = /* @__PURE__ */ __exportAll({ make: () => make$1 });
15
+ const make$1 = (_command, decide) => {
57
16
  return decide;
58
17
  };
59
18
  //#endregion
60
- //#region src/CanonicalDecide.workflow.ts
61
- /**
62
- * The canonical command. `Workflow.make` constrains its first argument to a real
63
- * schema class, so the canonical description needs one too — it carries no fields
64
- * because the canonical's phases do nothing, and its only job is to be a genuine
65
- * command value rather than a shape asserted into place.
66
- *
67
- * It is declared here rather than in a `*.schema.ts` because this is the owning
68
- * single-segment `<stem>.workflow.ts`, which `schema-declaration-location` admits.
69
- */
70
- var CanonicalCommand = class extends S.TaggedClass()("CanonicalCommand", {}) {};
71
- /**
72
- * The canonical decider. Extracted so `make-file-location` only sees it inside a
73
- * single-segment `.workflow.ts` file, satisfying the restored taxonomy while
74
- * preserving the exact phantom-channel contract the `DecidePhase` brand and
75
- * the interpreter rely on.
76
- */
77
- const canonicalDecide = make(CanonicalCommand, (_command) => Result$1.succeed(void 0));
78
- //#endregion
79
19
  //#region src/Cell.ts
80
20
  var Cell_exports = /* @__PURE__ */ __exportAll({
21
+ CellTypeId: () => CellTypeId,
81
22
  DESCRIPTION_MODULE: () => DESCRIPTION_MODULE,
82
23
  IO_CELLS: () => IO_CELLS,
83
- apply: () => apply,
84
- canonical: () => canonical,
85
- decide: () => decide,
86
- decode: () => decode,
87
- encode: () => encode,
88
- read: () => read,
24
+ andThen: () => andThen,
25
+ layer: () => layer,
26
+ map: () => map,
27
+ mapInput: () => mapInput,
28
+ provide: () => provide,
29
+ run: () => run,
89
30
  vocabulary: () => vocabulary,
90
- write: () => write
31
+ withPolicy: () => withPolicy,
32
+ zip: () => zip
91
33
  });
92
- /** The description package's own module name — what an import edge would match. */
93
- const DESCRIPTION_MODULE = "@systemfsoftware/effect-cell-types";
94
34
  /**
95
- * The I/O-cell classification: the cells whose calls are I/O, plus the non-cell module
96
- * sources whose calls are I/O. Written once here; a consumer folds it off the value.
35
+ * The nominal brand every `Cell` carries. `Cell.layer` is the only door that applies it.
97
36
  */
98
- const IO_CELLS = {
99
- cells: ["store", "adapter"],
100
- sources: ["effect/Clock", "effect/System"]
101
- };
102
- const READ_DONE = "call read(command) before decode(raw)";
103
- const DECODE_DONE = "call decode(raw) before decide(decoded)";
104
- const DECIDE_DONE = "call decide(decoded) before encode(decision)";
105
- const ENCODE_DONE = "call encode(decision) before write(output)";
106
- const WRITE_DONE = "call write(output) before applying the description";
107
- /** Replaces the open layer with itself plus one more phase record. */
108
- const intoOpenLayer = (description, phase) => {
109
- const layers = description.layers;
110
- const last = layers[layers.length - 1];
111
- return {
112
- module: description.module,
113
- ioCells: description.ioCells,
114
- layers: [...layers.slice(0, -1), {
115
- ...last,
116
- phases: [...last?.phases ?? [], phase]
117
- }]
118
- };
119
- };
37
+ const CellTypeId = Symbol.for("@systemfsoftware/effect-cell-types/Cell");
38
+ const make = (run) => ({
39
+ [CellTypeId]: CellTypeId,
40
+ run
41
+ });
120
42
  /**
121
- * Opens a layer. Passing a prior `WriteDone` opens a second layer over the same bag, so a
122
- * call site whose real order writes before it can classify is one description carrying two
123
- * layers rather than two descriptions composed by hand.
124
- *
125
- * This one is not dual: it starts the chain, so on the opening layer it has no `self` to
126
- * receive. Every phase after it is dual, which is what lets a description be written in the
127
- * order it runs.
43
+ * The interpreter. Order is the text: read, then decode, then decide, then encode, then
44
+ * write. The `E` channel is the sandwich's truth read, decode, and write failures;
45
+ * a decide refusal is the outcome the encode and write receive, not a failure.
128
46
  */
129
- const read = (run, previous) => ({
130
- [READ_DONE]: true,
131
- module: DESCRIPTION_MODULE,
132
- ioCells: IO_CELLS,
133
- layers: [...previous?.layers ?? [], { phases: [{
134
- name: "read",
135
- kind: "impure",
136
- convention: "effect",
137
- run
138
- }] }]
139
- });
47
+ const layerRunner = (spec) => {
48
+ if ("decode" in spec && "encode" in spec) return (input) => Effect$1.gen(function* () {
49
+ const raw = yield* spec.read(input);
50
+ const decoded = yield* Result$1.match(spec.decode(raw), {
51
+ onFailure: Effect$1.fail,
52
+ onSuccess: Effect$1.succeed
53
+ });
54
+ const outcome = spec.decide(decoded);
55
+ return yield* spec.write(spec.encode(outcome), raw);
56
+ });
57
+ return (input) => Effect$1.gen(function* () {
58
+ const raw = yield* spec.read(input);
59
+ const outcome = spec.decide(raw);
60
+ return yield* spec.write(outcome, raw);
61
+ });
62
+ };
63
+ function layer(spec) {
64
+ return make(layerRunner(spec));
65
+ }
66
+ const run = dual(2, (self, input) => self.run(input));
140
67
  /**
141
- * The chaining phases are dual, data-last overload declared first. Nesting the constructors
142
- * reads innermost-first — backwards from the order the phases run — which defeats the point of
143
- * a type that exists to make that order legible. In `pipe` the call site reads in phase order,
144
- * and the sentence still arrives as a missing member through it.
145
- *
146
- * A `Do`-notation scope binding each phase's result for later phases to read was measured as
147
- * an alternative: the sentence survives it, even with the scope varying per stage, because an
148
- * absent member is reported before type arguments are compared. It was not adopted, because a
149
- * scope the interpreter folds over is type-erased, and reading a phase back out of it needs an
150
- * assertion this design does not.
68
+ * Transforms the Cell's response.
151
69
  */
152
- const decode = dual(2, (previous, run) => ({
153
- [DECODE_DONE]: true,
154
- ...intoOpenLayer(previous, {
155
- name: "decode",
156
- kind: "pure",
157
- convention: "either-fail",
158
- run
159
- })
160
- }));
161
- const decide = dual(2, (previous, run) => ({
162
- [DECIDE_DONE]: true,
163
- ...intoOpenLayer(previous, {
164
- name: "decide",
165
- kind: "pure",
166
- convention: "either-pass",
167
- run
168
- })
169
- }));
170
- const encode = dual(2, (previous, run) => ({
171
- [ENCODE_DONE]: true,
172
- ...intoOpenLayer(previous, {
173
- name: "encode",
174
- kind: "pure",
175
- convention: "total",
176
- run
177
- })
178
- }));
179
- const write = dual(2, (previous, run) => ({
180
- [WRITE_DONE]: true,
181
- ...intoOpenLayer(previous, {
182
- name: "write",
183
- kind: "impure",
184
- convention: "effect",
185
- run
186
- })
187
- }));
70
+ const map = dual(2, (self, f) => make((input) => Effect$1.map(self.run(input), f)));
188
71
  /**
189
- * The runtime guard that lets the `'total'` case call `EncodePhase` soundly: an encode
190
- * phase is chained only after a decide, so the value reaching it is the outcome `Result`
191
- * and `Result.isResult` certifies exactly that. The specific `decision`/`decisionError`
192
- * members are not observable at runtime, so the guard narrows to them by construction —
193
- * the same trust the fold places in the chain's order.
72
+ * Transforms the Cell's input.
194
73
  */
195
- const isOutcome = (value) => Result$1.isResult(value);
74
+ const mapInput = dual(2, (self, f) => make((input) => self.run(f(input))));
196
75
  /**
197
- * Runs one layer as the sandwich the value declares: the phase records run in array
198
- * order, each dispatched on its carried `convention`. There is no phase sequence here —
199
- * the order is the `phases` array and the invocation shape is the `convention` field,
200
- * so a description's declared order IS its execution order. The convention switch is
201
- * exhaustive over the union via its `never` default: a future phase with an invocation
202
- * shape this module does not know fails at compile time, at this one location.
203
- *
204
- * The two `Failure` rules are carried by the phase types rather than chosen here. A `decode`
205
- * Failure has no downstream consumer — nothing accepts `decodeError` — so its only route is a
206
- * failure, which is what puts it in the derived error channel. A `decide` Failure cannot be
207
- * unwrapped, because `EncodePhase` takes the whole `Result`, so its only route is forward as
208
- * a value. Neither is a decision the interpreter makes.
209
- *
210
- * Every layer reachable from a `WriteDone` was built by the five constructors in order, so
211
- * its last phase is a write and every slot is filled. A layer that is nonetheless empty or
212
- * not closed by a write is a defect in this module, never a domain outcome, so it dies —
213
- * the same guard the name-keyed layer used for unfilled slots. `Effect.die` returns
214
- * `Effect<never>`, which is why the guards cost the derived `E` and `R` nothing.
76
+ * Feeds this Cell's response to the next Cell as its input. The error and service channels
77
+ * union.
215
78
  */
216
- const runLayer = (layer, command) => Effect$1.gen(function* () {
217
- const phases = layer.phases;
218
- const last = phases[phases.length - 1];
219
- if (!last || last.name !== "write") return yield* Effect$1.die(/* @__PURE__ */ new Error("effect-cell-types: a layer reached the interpreter without a write phase closing it"));
220
- let value = command;
221
- let raw = command;
222
- for (const phase of phases.slice(0, -1)) switch (phase.convention) {
223
- case "effect":
224
- if (phase.name === "read") {
225
- value = yield* phase.run(value);
226
- raw = value;
227
- break;
228
- }
229
- value = yield* phase.run(value, raw);
230
- break;
231
- case "either-fail":
232
- value = yield* Result$1.match(phase.run(value), {
233
- onFailure: Effect$1.fail,
234
- onSuccess: Effect$1.succeed
235
- });
236
- break;
237
- case "either-pass":
238
- value = phase.run(value);
239
- break;
240
- case "total":
241
- if (!isOutcome(value)) return yield* Effect$1.die(/* @__PURE__ */ new Error("effect-cell-types: an encode phase received a value that is not the decide outcome"));
242
- value = phase.run(value);
243
- break;
244
- default: {
245
- const unreachable = phase;
246
- return yield* Effect$1.die(/* @__PURE__ */ new Error(`effect-cell-types: unknown phase convention ${String(unreachable)}`));
247
- }
248
- }
249
- return yield* last.run(value, raw);
250
- });
79
+ const andThen = dual(2, (self, that) => make((input) => Effect$1.flatMap(self.run(input), (response) => that.run(response))));
251
80
  /**
252
- * Applies a description. The return type is deliberately not annotated: `gen` accumulates
253
- * `E` and `R` from the union of what is actually yielded, so an over-claimed channel is
254
- * unrepresentable rather than merely discouraged. Annotating it here would let this module
255
- * promise a failure that no phase can produce.
256
- *
257
- * The parameter keeps the terminal `WriteDone<P>` brand. It is not decoration: it is what
258
- * makes applying a half-built chain — a `ReadDone`, say — a compile error rather than a
259
- * runtime death, which `test-types/Cell.tst.ts` pins. The brand does not constrain the
260
- * phases array's order (a literal satisfies it in any order, which is why the interpreter
261
- * reads the order off the value), but it does constrain chain completion, and that is a
262
- * guarantee worth the narrower parameter.
263
- *
264
- * `forEach` takes no concurrency option here, so the layers run in declared order and the
265
- * sequence is structural rather than something a caller could pass differently; the
266
- * description's response is the last layer's. No scope is opened and interruptibility is
267
- * untouched, so a `Scope.Scope` a phase requires reaches the caller as part of the derived `R`.
81
+ * Runs both Cells against the same input and tuples the responses. Fails fast: when one
82
+ * side refuses, the other's write never runs.
268
83
  */
269
- const apply = (description, command) => Effect$1.gen(function* () {
270
- const responses = yield* Effect$1.forEach(description.layers, (layer) => runLayer(layer, command));
271
- return yield* Option.match(Arr.last(responses), {
272
- onNone: () => Effect$1.die(/* @__PURE__ */ new Error("effect-cell-types: a description reached the interpreter with no layers")),
273
- onSome: Effect$1.succeed
274
- });
275
- });
84
+ const zip = dual(2, (self, that) => make((input) => Effect$1.zipWith(self.run(input), that.run(input), (a, b) => [a, b])));
276
85
  /**
277
- * A canonical description, built through the public constructors with phases that do
278
- * nothing. It is exported so a consumer a generator, a lint rule, a documenter — can
279
- * obtain a real branded description without replaying the constructor chain: spread it
280
- * and substitute its phase records' `run`s. The records it carries are the same literals
281
- * the constructors write for real call sites, so the vocabulary below cannot drift from
282
- * them.
283
- *
284
- * Its order is not a choice this module makes. The stage brands admit exactly one chain, so
285
- * any other sequence fails to typecheck here — which is what keeps the derived order
286
- * non-circular: it is read off a value, and the value's shape is enforced by the types.
86
+ * Provides a Layer to the Cell, eliminating the services the layer builds from `R`. This is
87
+ * the one composition-root elimination; the resulting Cell still demands the layer's input
88
+ * services. A missing provide is a compile error at the run site.
287
89
  */
288
- const canonical = write(encode(decide(decode(read(() => Effect$1.void), () => Result$1.succeed(CanonicalCommand.make({}))), canonicalDecide), () => void 0), () => Effect$1.void);
90
+ const provide = dual(2, (self, layer) => make((input) => Effect$1.provide(self.run(input), layer)));
289
91
  /**
290
- * The phase vocabulary, obtained by walking `canonical`. A consumer that needs the phase
291
- * names, their purity, or their order — a lint rule, a generator, a document — reads them
292
- * from here instead of restating them, so there is one place a phase is described and the
293
- * description is the place.
92
+ * Wraps the Cell's run in a `Policy` retry, timeout, and their kin — preserving every
93
+ * channel.
294
94
  */
295
- const WALKED_PHASES = Arr.flatMap(canonical.layers, (layer) => layer.phases.map(({ convention, kind, name }) => ({
296
- convention,
297
- kind,
298
- name
299
- })));
95
+ const withPolicy = dual(2, (self, policy) => make((input) => policy(self.run(input))));
300
96
  const vocabulary = {
301
- module: canonical.module,
302
- ioCells: canonical.ioCells,
303
- phases: WALKED_PHASES,
304
- byKind: {
305
- pure: WALKED_PHASES.filter((phase) => phase.kind === "pure").map((phase) => phase.name),
306
- impure: WALKED_PHASES.filter((phase) => phase.kind === "impure").map((phase) => phase.name)
307
- },
308
- applier: "apply"
97
+ module: DESCRIPTION_MODULE,
98
+ ioCells: IO_CELLS,
99
+ byKind: { pure: [
100
+ "decode",
101
+ "decide",
102
+ "encode"
103
+ ] },
104
+ composer: "layer"
309
105
  };
310
106
  //#endregion
311
107
  //#region src/Policy.ts
@@ -348,8 +144,10 @@ var Wire_exports = /* @__PURE__ */ __exportAll({
348
144
  * the schema library can express, including a vendor's own schema — deliberately.
349
145
  */
350
146
  const mint = (field) => {
147
+ assertMinted(field);
351
148
  return field;
352
149
  };
150
+ function assertMinted(_field) {}
353
151
  const wire = (fields) => mint(Schema.Struct(fields));
354
152
  //#endregion
355
153
  export { Cell_exports as Cell, Policy_exports as Policy, Wire_exports as Wire, Workflow_exports as Workflow };
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@systemfsoftware/effect-cell-types",
3
3
  "license": "Apache-2.0",
4
- "version": "5.0.2",
4
+ "version": "6.0.1",
5
5
  "author": "Ryan Lee <drdgvhbh@gmail.com>",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/systemfsoftware/systemfsoftware.git",
9
- "directory": "packages/core/effect/cell/types"
9
+ "directory": "packages/effect-cell-types"
10
10
  },
11
- "homepage": "https://github.com/systemfsoftware/systemfsoftware/tree/main/packages/core/effect/cell/types#readme",
11
+ "homepage": "https://github.com/systemfsoftware/systemfsoftware/tree/main/packages/effect-cell-types#readme",
12
12
  "bugs": "https://github.com/systemfsoftware/systemfsoftware/issues",
13
- "description": "Type-level contracts for the repo's cell taxonomy, starting with the Workflow decision channel — branded Workflow<C, D, E> types plus a runtime make constructor every effect kernel can share.",
13
+ "description": "Type-level contracts for the repo's cell taxonomy, starting with the Workflow decision channel — branded Workflow<C, D, E> types plus a runtime make constructor shared across the repo's cells.",
14
14
  "keywords": [
15
15
  "effect",
16
16
  "effect-ts",
@@ -42,10 +42,10 @@
42
42
  "tsdown": "^0.22.14",
43
43
  "tstyche": "^7.1.0",
44
44
  "vitest": "^4",
45
- "@systemfsoftware/effect-gherkin-spec": "4.0.0",
46
- "@systemfsoftware/oxlint-config": "^0.1.0",
45
+ "@systemfsoftware/effect-gherkin-spec": "4.0.1",
46
+ "@systemfsoftware/vitest-config": "^0.1.0",
47
47
  "@systemfsoftware/tsconfig": "^1.3.3",
48
- "@systemfsoftware/vitest-config": "^0.1.0"
48
+ "@systemfsoftware/oxlint-config": "^0.1.0"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "effect": "^4.0.0-rc.112"