@systemfsoftware/effect-cell-types 1.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/LICENSE +203 -0
- package/README.md +215 -0
- package/dist/effect-cell-types.d.ts +49 -0
- package/dist/index.d.ts +419 -0
- package/dist/index.mjs +359 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Copyright (c) 2026 Ryan Lee (systemfsoftware)
|
|
2
|
+
|
|
3
|
+
Apache License
|
|
4
|
+
Version 2.0, January 2004
|
|
5
|
+
http://www.apache.org/licenses/
|
|
6
|
+
|
|
7
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
8
|
+
|
|
9
|
+
1. Definitions.
|
|
10
|
+
|
|
11
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
12
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
13
|
+
|
|
14
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
15
|
+
the copyright owner that is granting the License.
|
|
16
|
+
|
|
17
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
18
|
+
other entities that control, are controlled by, or are under common
|
|
19
|
+
control with that entity. For the purposes of this definition,
|
|
20
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
21
|
+
direction or management of such entity, whether by contract or
|
|
22
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
23
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
24
|
+
|
|
25
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
26
|
+
exercising permissions granted by this License.
|
|
27
|
+
|
|
28
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
29
|
+
including but not limited to software source code, documentation
|
|
30
|
+
source, and configuration files.
|
|
31
|
+
|
|
32
|
+
"Object" form shall mean any form resulting from mechanical
|
|
33
|
+
transformation or translation of a Source form, including but
|
|
34
|
+
not limited to compiled object code, generated documentation,
|
|
35
|
+
and conversions to other media types.
|
|
36
|
+
|
|
37
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
38
|
+
Object form, made available under the License, as indicated by a
|
|
39
|
+
copyright notice that is included in or attached to the work
|
|
40
|
+
(an example is provided in the Appendix below).
|
|
41
|
+
|
|
42
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
43
|
+
form, that is based on (or derived from) the Work and for which the
|
|
44
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
45
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
46
|
+
of this License, Derivative Works shall not include works that remain
|
|
47
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
48
|
+
the Work and Derivative Works thereof.
|
|
49
|
+
|
|
50
|
+
"Contribution" shall mean any work of authorship, including
|
|
51
|
+
the original version of the Work and any modifications or additions
|
|
52
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
53
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
54
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
55
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
56
|
+
means any form of electronic, verbal, or written communication sent
|
|
57
|
+
to the Licensor or its representatives, including but not limited to
|
|
58
|
+
communication on electronic mailing lists, source code control systems,
|
|
59
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
60
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
61
|
+
excluding communication that is conspicuously marked or otherwise
|
|
62
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
63
|
+
|
|
64
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
65
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
66
|
+
subsequently incorporated within the Work.
|
|
67
|
+
|
|
68
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
69
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
70
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
71
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
72
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
73
|
+
Work and such Derivative Works in Source or Object form.
|
|
74
|
+
|
|
75
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
76
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
77
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
78
|
+
(except as stated in this section) patent license to make, have made,
|
|
79
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
80
|
+
where such license applies only to those patent claims licensable
|
|
81
|
+
by such Contributor that are necessarily infringed by their
|
|
82
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
83
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
84
|
+
institute patent litigation against any entity (including a
|
|
85
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
86
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
87
|
+
or contributory patent infringement, then any patent licenses
|
|
88
|
+
granted to You under this License for that Work shall terminate
|
|
89
|
+
as of the date such litigation is filed.
|
|
90
|
+
|
|
91
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
92
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
93
|
+
modifications, and in Source or Object form, provided that You
|
|
94
|
+
meet the following conditions:
|
|
95
|
+
|
|
96
|
+
(a) You must give any other recipients of the Work or
|
|
97
|
+
Derivative Works a copy of this License; and
|
|
98
|
+
|
|
99
|
+
(b) You must cause any modified files to carry prominent notices
|
|
100
|
+
stating that You changed the files; and
|
|
101
|
+
|
|
102
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
103
|
+
that You distribute, all copyright, patent, trademark, and
|
|
104
|
+
attribution notices from the Source form of the Work,
|
|
105
|
+
excluding those notices that do not pertain to any part of
|
|
106
|
+
the Derivative Works; and
|
|
107
|
+
|
|
108
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
109
|
+
distribution, then any Derivative Works that You distribute must
|
|
110
|
+
include a readable copy of the attribution notices contained
|
|
111
|
+
within such NOTICE file, excluding those notices that do not
|
|
112
|
+
pertain to any part of the Derivative Works, in at least one
|
|
113
|
+
of the following places: within a NOTICE text file distributed
|
|
114
|
+
as part of the Derivative Works; within the Source form or
|
|
115
|
+
documentation, if provided along with the Derivative Works; or,
|
|
116
|
+
within a display generated by the Derivative Works, if and
|
|
117
|
+
wherever such third-party notices normally appear. The contents
|
|
118
|
+
of the NOTICE file are for informational purposes only and
|
|
119
|
+
do not modify the License. You may add Your own attribution
|
|
120
|
+
notices within Derivative Works that You distribute, alongside
|
|
121
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
122
|
+
that such additional attribution notices cannot be construed
|
|
123
|
+
as modifying the License.
|
|
124
|
+
|
|
125
|
+
You may add Your own copyright statement to Your modifications and
|
|
126
|
+
may provide additional or different license terms and conditions
|
|
127
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
128
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
129
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
130
|
+
the conditions stated in this License.
|
|
131
|
+
|
|
132
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
133
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
134
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
135
|
+
this License, without any additional terms or conditions.
|
|
136
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
137
|
+
the terms of any separate license agreement you may have executed
|
|
138
|
+
with Licensor regarding such Contributions.
|
|
139
|
+
|
|
140
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
141
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
142
|
+
except as required for reasonable and customary use in describing the
|
|
143
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
144
|
+
|
|
145
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
146
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
147
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
148
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
149
|
+
implied, including, without limitation, any warranties or conditions
|
|
150
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
151
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
152
|
+
appropriateness of using or redistributing the Work and assume any
|
|
153
|
+
risks associated with Your exercise of permissions under this License.
|
|
154
|
+
|
|
155
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
156
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
157
|
+
unless required by applicable law (such as deliberate and grossly
|
|
158
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
159
|
+
liable to You for damages, including any direct, indirect, special,
|
|
160
|
+
incidental, or consequential damages of any character arising as a
|
|
161
|
+
result of this License or out of the use or inability to use the
|
|
162
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
163
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
164
|
+
other commercial damages or losses), even if such Contributor
|
|
165
|
+
has been advised of the possibility of such damages.
|
|
166
|
+
|
|
167
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
168
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
169
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
170
|
+
or other liability obligations and/or rights consistent with this
|
|
171
|
+
License. However, in accepting such obligations, You may act only
|
|
172
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
173
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
174
|
+
defend, and hold each Contributor harmless for any liability
|
|
175
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
176
|
+
of your accepting any such warranty or additional liability.
|
|
177
|
+
|
|
178
|
+
END OF TERMS AND CONDITIONS
|
|
179
|
+
|
|
180
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
181
|
+
|
|
182
|
+
To apply the Apache License to your work, attach the following
|
|
183
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
184
|
+
replaced with your own identifying information. (Don't include
|
|
185
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
186
|
+
comment syntax for the file format. We also recommend that a
|
|
187
|
+
file or class name and description of purpose be included on the
|
|
188
|
+
same "printed page" as the copyright notice for easier
|
|
189
|
+
identification within third-party archives.
|
|
190
|
+
|
|
191
|
+
Copyright 2026 Ryan Lee (systemfsoftware)
|
|
192
|
+
|
|
193
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
194
|
+
you may not use this file except in compliance with the License.
|
|
195
|
+
You may obtain a copy of the License at
|
|
196
|
+
|
|
197
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
198
|
+
|
|
199
|
+
Unless required by applicable law or agreed to in writing, software
|
|
200
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
201
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
202
|
+
See the License for the specific language governing permissions and
|
|
203
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# @systemfsoftware/effect-cell-types
|
|
2
|
+
|
|
3
|
+
The type-level contract for a `*.workflow.ts` cell. A workflow is a pure decision — a command in, a `Result` out — and `Workflow<Command, Decision, Error>` pins that shape in the type system. The contract is checked by `tsc` from the file's **content** (an exported value that violates the shape stops the build), not by a lint rule keyed on the file's **name**. Beside the types the package ships exactly one runtime value — the identity constructor `make` — and the type tests (`test-types/Workflow.tst.ts`, run by tstyche) prove the channel guards still bind.
|
|
4
|
+
|
|
5
|
+
## The contract
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import type { Workflow } from '@systemfsoftware/effect-cell-types'
|
|
9
|
+
|
|
10
|
+
type Decide = Workflow<Command, Decision, Error>
|
|
11
|
+
// = ((command: Command) => Result<Decision, Error>) & WorkflowBrand
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
When both channels are inhabited, `Workflow<Command, Decision, Error>` is the function type
|
|
15
|
+
`(command: Command) => Result<Decision, Error>` carrying the nominal `WorkflowBrand`
|
|
16
|
+
conjunct — a phantom readonly TypeId-keyed field that no runtime property backs. The brand
|
|
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
|
|
21
|
+
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).
|
|
23
|
+
|
|
24
|
+
## The constructor
|
|
25
|
+
|
|
26
|
+
Executors build a workflow from a plain decider with `make` — runtime identity, one cast across the branded return:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { make } from '@systemfsoftware/effect-cell-types'
|
|
30
|
+
import { Result } from 'effect'
|
|
31
|
+
|
|
32
|
+
export const decide = make<DecideInput, RestartDecision, RestartDecisionExhausted>(
|
|
33
|
+
(
|
|
34
|
+
input,
|
|
35
|
+
) => (input.exitSuccess
|
|
36
|
+
? Result.succeed(new RestartDecisionContinue())
|
|
37
|
+
: Result.fail(new RestartDecisionExhausted())),
|
|
38
|
+
)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The parameter is the plain function type, not `Workflow<C, D, E>`: the `never`-channel conditional lives on the **return** type, so a total decision (`Result<Decision, never>`) resolves to `UninhabitedError` and the call site fails with "This expression is not callable", while a `Promise`- or bare-value-returning decider is rejected at the argument. `make` is a runtime value, so consumers need it as an ordinary import only where they construct workflows; everywhere else `import type` still erases at compile time.
|
|
42
|
+
|
|
43
|
+
## Worked example
|
|
44
|
+
|
|
45
|
+
`decideRestart` in `@systemfsoftware/effect-daemon-spec` (`src/internal/restart-decision.workflow.ts`) is the real consumer — a supervisor deciding what to do when a child exits. The sibling schema file, which defines `DecideInput` and `RestartStrategy`, is elided here.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { Workflow } from '@systemfsoftware/effect-cell-types'
|
|
49
|
+
import { Result } from 'effect'
|
|
50
|
+
import * as Match from 'effect/Match'
|
|
51
|
+
import * as S from 'effect/Schema'
|
|
52
|
+
|
|
53
|
+
const RestartDecisionTypeId: unique symbol = Symbol.for(
|
|
54
|
+
'@systemfsoftware/effect-daemon/RestartDecision',
|
|
55
|
+
)
|
|
56
|
+
type RestartDecisionTypeId = typeof RestartDecisionTypeId
|
|
57
|
+
|
|
58
|
+
export class RestartDecisionContinue extends S.TaggedClass<RestartDecisionContinue>()('Continue', {}) {
|
|
59
|
+
readonly [RestartDecisionTypeId] = RestartDecisionTypeId
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class RestartDecisionRestart extends S.TaggedClass<RestartDecisionRestart>()('Restart', {
|
|
63
|
+
indices: S.NonEmptyArray(S.Int),
|
|
64
|
+
}) {
|
|
65
|
+
readonly [RestartDecisionTypeId] = RestartDecisionTypeId
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class RestartDecisionExhausted extends S.TaggedError<RestartDecisionExhausted>()('Exhausted', {}) {
|
|
69
|
+
readonly [RestartDecisionTypeId] = RestartDecisionTypeId
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const restartIndicesFor = (
|
|
73
|
+
strategy: RestartStrategy,
|
|
74
|
+
failedIndex: number,
|
|
75
|
+
total: number,
|
|
76
|
+
): readonly [number, ...readonly number[]] =>
|
|
77
|
+
Match.value(strategy).pipe(
|
|
78
|
+
Match.when('one_for_one', () => [failedIndex] as const),
|
|
79
|
+
Match.when(
|
|
80
|
+
'one_for_all',
|
|
81
|
+
() => [0, ...Array.from({ length: Math.max(0, total - 1) }, (_, i) => i + 1)] as const,
|
|
82
|
+
),
|
|
83
|
+
Match.when(
|
|
84
|
+
'rest_for_one',
|
|
85
|
+
() =>
|
|
86
|
+
[
|
|
87
|
+
failedIndex,
|
|
88
|
+
...Array.from({ length: Math.max(0, total - failedIndex - 1) }, (_, i) => failedIndex + 1 + i),
|
|
89
|
+
] as const,
|
|
90
|
+
),
|
|
91
|
+
Match.exhaustive,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
export const decideRestart = Workflow.make(
|
|
95
|
+
(input: DecideInput): Result.Result<
|
|
96
|
+
RestartDecisionContinue | RestartDecisionRestart,
|
|
97
|
+
RestartDecisionExhausted
|
|
98
|
+
> =>
|
|
99
|
+
Match.value(input).pipe(
|
|
100
|
+
Match.when({ exitSuccess: true }, () => Result.succeed(new RestartDecisionContinue())),
|
|
101
|
+
Match.when(
|
|
102
|
+
{ exitSuccess: false, intensityExceeded: true },
|
|
103
|
+
() => Result.fail(new RestartDecisionExhausted()),
|
|
104
|
+
),
|
|
105
|
+
Match.orElse(() =>
|
|
106
|
+
Result.succeed(
|
|
107
|
+
new RestartDecisionRestart({
|
|
108
|
+
indices: restartIndicesFor(input.strategy, input.failedIndex, input.totalChildren),
|
|
109
|
+
}),
|
|
110
|
+
)
|
|
111
|
+
),
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The shape to copy: one exported decision built by `Workflow.make`, whose body returns
|
|
117
|
+
`Result` values via `Result.succeed` and `Result.fail`. The constructor infers the channels
|
|
118
|
+
from the annotated return and is the only door to the `WorkflowBrand` conjunct — annotating
|
|
119
|
+
a function `Workflow<…>` directly is now refused wherever the brand is demanded, because a
|
|
120
|
+
workflow that never passed through `make` is not a decision anything may run. The error
|
|
121
|
+
channel is a real variant (`RestartDecisionExhausted`) — giving up is a decision the caller
|
|
122
|
+
must branch on, so declaring the error channel `never` is rejected, not allowed.
|
|
123
|
+
|
|
124
|
+
## What it rejects at compile time
|
|
125
|
+
|
|
126
|
+
All five violations fail `tsc`; the messages below are what `tsc` reports (verified against this package and `effect@4.0.0-rc.108`).
|
|
127
|
+
|
|
128
|
+
| Violation | `tsc` reports | Why it is rejected |
|
|
129
|
+
| -------------------------------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
130
|
+
| 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 |
|
|
131
|
+
| 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 |
|
|
132
|
+
| `never` decision channel | `Type '...' is not assignable to type 'UninhabitedDecision'` | a workflow that can never produce a decision can never succeed |
|
|
133
|
+
| `never` error channel | `Type '...' is not assignable to type 'UninhabitedError'` | a workflow that cannot fail decides nothing; move it to a `*.kernel.ts` |
|
|
134
|
+
| 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 |
|
|
135
|
+
|
|
136
|
+
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:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
export interface UninhabitedDecision {
|
|
140
|
+
readonly __WORKFLOW_DECISION_CHANNEL_IS_NEVER__:
|
|
141
|
+
'this workflow can never succeed; give it a decision variant it can return'
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface UninhabitedError {
|
|
145
|
+
readonly __WORKFLOW_ERROR_CHANNEL_IS_NEVER__:
|
|
146
|
+
'this workflow cannot fail, so it decides nothing; give it an error variant or move it to a *.kernel.ts'
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The `never` checks use `[Decision] extends [never]`, not `Decision extends never`: the tuple wrap stops conditional-type distribution, without which `never` satisfies the conditional vacuously and the marker is never reached.
|
|
151
|
+
|
|
152
|
+
## Result.gen bodies work — and are checked more tightly
|
|
153
|
+
|
|
154
|
+
A `Workflow.make` body may be a `Result.gen` generator, so long as it stays one path:
|
|
155
|
+
the failing outcome rides an arm of an exhaustive dispatch, and the generator yields
|
|
156
|
+
the outcome once:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import { Workflow } from '@systemfsoftware/effect-cell-types'
|
|
160
|
+
import { Result } from 'effect'
|
|
161
|
+
import * as Match from 'effect/Match'
|
|
162
|
+
|
|
163
|
+
class Decision {}
|
|
164
|
+
class Err {
|
|
165
|
+
constructor(readonly reason: string) {}
|
|
166
|
+
}
|
|
167
|
+
interface Input {
|
|
168
|
+
readonly valid: boolean
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const decide = Workflow.make(
|
|
172
|
+
(input: Input): Result.Result<Decision, Err> =>
|
|
173
|
+
Result.gen(function*() {
|
|
174
|
+
const outcome = Match.value(input).pipe(
|
|
175
|
+
Match.when({ valid: false }, () => Result.fail(new Err('invalid input'))),
|
|
176
|
+
Match.orElse(() => Result.succeed(new Decision())),
|
|
177
|
+
)
|
|
178
|
+
return yield* outcome
|
|
179
|
+
}),
|
|
180
|
+
)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
A conditional `yield*` — an `if` or ternary that places a failing `yield*` on one path —
|
|
184
|
+
opens a second path inside the decision and is refused by the `make-body-purity` lint
|
|
185
|
+
rule; the failure must live on one path of the dispatch (or a first-statement guard that
|
|
186
|
+
converges immediately), never behind a mid-body branch.
|
|
187
|
+
|
|
188
|
+
`Result.gen` infers its error channel from the union of the `Result`s the body yields,
|
|
189
|
+
so the failing arm above makes the inference exactly `Err` and the declaration holds. A
|
|
190
|
+
body with **no** failing yield infers `unknown`, which does not satisfy a declared error
|
|
191
|
+
type — so an unreachable error channel is rejected rather than silently allowed:
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
const decide = Workflow.make(
|
|
195
|
+
(input: Input): Result.Result<Decision, Err> =>
|
|
196
|
+
Result.gen(function*() {
|
|
197
|
+
return new Decision()
|
|
198
|
+
}),
|
|
199
|
+
)
|
|
200
|
+
// tsc: Type 'Result<Decision, unknown>' is not assignable to type 'Result<Decision, Err>'
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
If the workflow genuinely cannot fail, the error channel says so — and that is a `*.kernel.ts`, not a workflow.
|
|
204
|
+
|
|
205
|
+
## A wrong channel breaks the whole consumer cone
|
|
206
|
+
|
|
207
|
+
Measured on the real consumer: when `decideRestart`'s error channel was set to `never`, `tsc` produced errors in the workflow file, 11 in its property test, and 4 in a downstream executor (`supervisor-body.executor.ts`). A lint rule flags one file; a type breaks every consumer. The workflow file, its tests, and everything that calls it fail together, at compile time, before anything runs.
|
|
208
|
+
|
|
209
|
+
## Install
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
pnpm add -D @systemfsoftware/effect-cell-types
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
A devDependency — consumers mostly use the types (`import type`), and the one runtime export, `make`, is an identity function. `effect` is a peer dependency: bring your own (you already have it).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Either } from 'effect/Either';
|
|
2
|
+
|
|
3
|
+
declare type Alt = {
|
|
4
|
+
readonly [Alternative]: true;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
declare const Alternative: unique symbol;
|
|
8
|
+
|
|
9
|
+
declare type Cmd = {
|
|
10
|
+
readonly [Command]: true;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
declare const Command: unique symbol;
|
|
14
|
+
|
|
15
|
+
declare type Dec = {
|
|
16
|
+
readonly [Decision]: true;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
declare const Decision: unique symbol;
|
|
20
|
+
|
|
21
|
+
export declare type DecisionUnionSurvivesDistribution = Holds<Identical<Workflow<Cmd, Dec | Alt, Err>, (command: Cmd) => Either<Dec | Alt, Err>>>;
|
|
22
|
+
|
|
23
|
+
declare type Err = {
|
|
24
|
+
readonly [Failure]: true;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
declare const Failure: unique symbol;
|
|
28
|
+
|
|
29
|
+
declare type Holds<Claim extends true> = Claim;
|
|
30
|
+
|
|
31
|
+
declare type Identical<Left, Right> = (<G>() => G extends Left ? 1 : 2) extends (<G>() => G extends Right ? 1 : 2) ? true : false;
|
|
32
|
+
|
|
33
|
+
export declare type InhabitedWorkflowIsCallable = Holds<Identical<Workflow<Cmd, Dec, Err>, (command: Cmd) => Either<Dec, Err>>>;
|
|
34
|
+
|
|
35
|
+
export declare type NeverDecisionIsRejected = Holds<Identical<Workflow<Cmd, never, Err>, UninhabitedDecision>>;
|
|
36
|
+
|
|
37
|
+
export declare type NeverErrorIsRejected = Holds<Identical<Workflow<Cmd, Dec, never>, UninhabitedError>>;
|
|
38
|
+
|
|
39
|
+
export declare interface UninhabitedDecision {
|
|
40
|
+
readonly __WORKFLOW_DECISION_CHANNEL_IS_NEVER__: 'this workflow can never succeed; give it a decision variant it can return';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export declare interface UninhabitedError {
|
|
44
|
+
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';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export declare type Workflow<Command, Decision, DecisionError> = [Decision] extends [never] ? UninhabitedDecision : [DecisionError] extends [never] ? UninhabitedError : (command: Command) => Either<Decision, DecisionError>;
|
|
48
|
+
|
|
49
|
+
export { }
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import * as Effect$1 from "effect/Effect";
|
|
2
|
+
import { Effect } from "effect/Effect";
|
|
3
|
+
import * as Result$1 from "effect/Result";
|
|
4
|
+
import { Result } from "effect/Result";
|
|
5
|
+
import { Schema, SchemaAST } from "effect";
|
|
6
|
+
declare namespace Workflow_d_exports {
|
|
7
|
+
export { Inhabited, Tagged, UninhabitedDecision, UninhabitedError, UntaggedError, Workflow, WorkflowBrand, make };
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The nominal brand a workflow carries. `Workflow.make` is the only door that applies it,
|
|
11
|
+
* and every surface that runs a decision — `Cell.decide`, and through it `DecideNode.run`
|
|
12
|
+
* via the `DecidePhase` conjunct — demands it, so a bare decider that skipped `make` is
|
|
13
|
+
* refused by the compiler at the call site that would have run it.
|
|
14
|
+
*
|
|
15
|
+
* The brand is phantom — a readonly TypeId-keyed field that no runtime property ever
|
|
16
|
+
* backs: `assertWorkflow` narrows the same function value without touching it. The
|
|
17
|
+
* `Symbol.for` key follows the repo's branded-class idiom (the instance brands in
|
|
18
|
+
* `restart-decision.workflow.ts`, `hook-verdict.workflow.ts` and `survivors.workflow.ts`)
|
|
19
|
+
* so it is stable across realms; this is the type-level brand that subsumes those
|
|
20
|
+
* instance brands on the workflow shapes.
|
|
21
|
+
*/
|
|
22
|
+
declare const WorkflowTypeId: unique symbol;
|
|
23
|
+
type WorkflowTypeId = typeof WorkflowTypeId;
|
|
24
|
+
/** The phantom property `Workflow<C,D,E>` and `Cell.DecidePhase<P>` carry. */
|
|
25
|
+
interface WorkflowBrand {
|
|
26
|
+
readonly [WorkflowTypeId]: WorkflowTypeId;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Marker a workflow resolves to when its decision channel is `never`. The property type is the
|
|
30
|
+
* fix, so the compiler diagnostic names it.
|
|
31
|
+
*/
|
|
32
|
+
interface UninhabitedDecision {
|
|
33
|
+
readonly __WORKFLOW_DECISION_CHANNEL_IS_NEVER__: 'this workflow can never succeed; give it a decision variant it can return';
|
|
34
|
+
}
|
|
35
|
+
/** Marker a workflow resolves to when its error channel is `never`. */
|
|
36
|
+
interface UninhabitedError {
|
|
37
|
+
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';
|
|
38
|
+
}
|
|
39
|
+
/** Marker a workflow resolves to when its error channel carries no tag to dispatch on. */
|
|
40
|
+
interface UntaggedError {
|
|
41
|
+
readonly __WORKFLOW_ERROR_CHANNEL_CARRIES_NO_TAG__: 'this error carries no _tag the consumer can dispatch on; declare it as an S.TaggedError';
|
|
42
|
+
}
|
|
43
|
+
/** The shape an error channel must have: a tag the consumer dispatches on. */
|
|
44
|
+
interface Tagged {
|
|
45
|
+
readonly _tag: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A decider whose channels are both inhabited, or the marker naming which channel is not.
|
|
49
|
+
*
|
|
50
|
+
* `[T] extends [never]` rather than `T extends never`: the tuple wrap stops distribution, without
|
|
51
|
+
* which a `never` channel satisfies the conditional vacuously and is never caught.
|
|
52
|
+
*
|
|
53
|
+
* The inhabited branch carries the nominal brand: `Workflow.make` is the only constructor that
|
|
54
|
+
* applies it, so a bare function annotated `Workflow<C, D, E>` is refused wherever the brand
|
|
55
|
+
* is demanded — which is exactly where a decision gets run.
|
|
56
|
+
*/
|
|
57
|
+
type Workflow<Command, Decision, DecisionError> = [Decision] extends [never] ? UninhabitedDecision : [DecisionError] extends [never] ? UninhabitedError : ((command: Command) => Result<Decision, DecisionError>) & WorkflowBrand;
|
|
58
|
+
/**
|
|
59
|
+
* `unknown` when both channels are inhabited and the error carries a tag, so the intersection in
|
|
60
|
+
* {@link make} collapses to the plain `Result` and neither inference nor the authoring surface
|
|
61
|
+
* changes. Otherwise the marker the author must satisfy, which they cannot, which is the point.
|
|
62
|
+
*/
|
|
63
|
+
type Inhabited<Decision, DecisionError> = [Decision] extends [never] ? UninhabitedDecision : [DecisionError] extends [never] ? UninhabitedError : [DecisionError] extends [Tagged] ? unknown : UntaggedError;
|
|
64
|
+
/**
|
|
65
|
+
* Builds a workflow, refusing an uninhabited or untagged channel at this call rather than at
|
|
66
|
+
* whoever first calls the result — which for a workflow nothing calls yet is never.
|
|
67
|
+
*
|
|
68
|
+
* The markers ride the parameter function's return type, not the parameter as `Workflow<C, D, E>`:
|
|
69
|
+
* a conditional type in parameter position resolves `D` and `E` to `unknown` and the markers become
|
|
70
|
+
* unreachable. On the return type both still infer from the `Result` conjunct while the marker
|
|
71
|
+
* conjunct is what an uninhabited channel fails to satisfy.
|
|
72
|
+
*
|
|
73
|
+
* `E` carries no constraint on purpose. Constraining it gives inference a fallback: where `E` would
|
|
74
|
+
* infer as `never` TypeScript substitutes the constraint instead, the conditional takes its
|
|
75
|
+
* inhabited branch, and a `never` channel passes. The tagged requirement therefore lives in
|
|
76
|
+
* {@link Inhabited}, where nothing can stand in for `never`.
|
|
77
|
+
*
|
|
78
|
+
* The narrowing goes through an assertion signature rather than an `as` cast: every narrowing
|
|
79
|
+
* assertion trips `typescript(no-unsafe-type-assertion)`, and a suppression comment would hide the
|
|
80
|
+
* one place this file could lie. It is sound rather than merely permitted — with both channels
|
|
81
|
+
* inhabited `Workflow<C, D, E>` is `(command: C) => Result<D, E>` carrying the {@link WorkflowBrand}
|
|
82
|
+
* conjunct, and otherwise the return type is a marker with no call signature, so the value handed
|
|
83
|
+
* back is unobservable through it. The brand is applied here and nowhere else: the assertion adds
|
|
84
|
+
* no runtime property, so `make` stays the identity function it always was, yet a value that did
|
|
85
|
+
* not pass through this door fails the conjunct wherever a decision is run.
|
|
86
|
+
*/
|
|
87
|
+
declare const make: <C, D, E>(decide: (command: C) => Result<D, E> & Inhabited<D, E>) => Workflow<C, D, E>;
|
|
88
|
+
declare namespace Cell_d_exports {
|
|
89
|
+
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 };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The type bag. Every phase's input and output type travels in one record so that a
|
|
93
|
+
* stage's own type arguments are identical across stages, which leaves the sentence
|
|
94
|
+
* member as the only difference a diagnostic can report. Measured: with the payload
|
|
95
|
+
* carried per-stage instead, the compiler reports the mismatch at the argument (TS2345)
|
|
96
|
+
* and the sentence arrives only as a type argument; with the payload in one bag it
|
|
97
|
+
* reports the missing member (TS2741) and the sentence is that member's name.
|
|
98
|
+
*/
|
|
99
|
+
interface Phases {
|
|
100
|
+
readonly command: unknown;
|
|
101
|
+
readonly raw: unknown;
|
|
102
|
+
readonly decoded: unknown;
|
|
103
|
+
readonly decision: unknown;
|
|
104
|
+
readonly decisionError: unknown;
|
|
105
|
+
readonly output: unknown;
|
|
106
|
+
readonly response: unknown;
|
|
107
|
+
readonly decodeError: unknown;
|
|
108
|
+
readonly readError: unknown;
|
|
109
|
+
readonly writeError: unknown;
|
|
110
|
+
readonly readContext: unknown;
|
|
111
|
+
readonly writeContext: unknown;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* A read gathers what the decision needs, and may gather a product across its interior;
|
|
115
|
+
* that interior is not type-visible, so no I/O count is claimed or enforced here. A step that
|
|
116
|
+
* mutates in order to report — bumping a counter and returning the resulting rate — is one
|
|
117
|
+
* such product, and belongs here rather than in a layer of its own.
|
|
118
|
+
*/
|
|
119
|
+
type ReadPhase<P extends Phases> = (command: P['command']) => Effect$1.Effect<P['raw'], P['readError'], P['readContext']>;
|
|
120
|
+
/** Validation. Its `Left` is fatal: it reaches the derived error channel and no write runs. */
|
|
121
|
+
type DecodePhase<P extends Phases> = (raw: P['raw']) => Result$1.Result<P['decoded'], P['decodeError']>;
|
|
122
|
+
/**
|
|
123
|
+
* The decision. Its `Left` is an outcome, not a fault: both branches travel on to the write.
|
|
124
|
+
*
|
|
125
|
+
* The {@link WorkflowBrand} conjunct makes this the one surface a decision must cross
|
|
126
|
+
* branded: only a `Workflow.make` value satisfies it, so a bare lambda handed here is
|
|
127
|
+
* refused by the compiler with the brand conjunct named in the diagnostic. The `run` on
|
|
128
|
+
* a `DecideNode` inherits the conjunct through this type, which keeps the interpreter's
|
|
129
|
+
* fold sound — a description can only carry decisions that came through `make`.
|
|
130
|
+
*/
|
|
131
|
+
type DecidePhase<P extends Phases> = ((decoded: P['decoded']) => Result$1.Result<P['decision'], P['decisionError']>) & WorkflowBrand;
|
|
132
|
+
/** Shapes what the write consumes. Total, so it receives both branches of the decision. */
|
|
133
|
+
type EncodePhase<P extends Phases> = (outcome: Result$1.Result<P['decision'], P['decisionError']>) => P['output'];
|
|
134
|
+
type WritePhase<P extends Phases> = (output: P['output']) => Effect$1.Effect<P['response'], P['writeError'], P['writeContext']>;
|
|
135
|
+
/**
|
|
136
|
+
* The invocation shape a folding consumer must use to call a phase's `run`:
|
|
137
|
+
* - `'effect'` — `run` returns an `Effect`; yield it. (read, write)
|
|
138
|
+
* - `'either-fail'` — `run` returns a `Result` whose `Failure` is fatal; fail on `Failure`. (decode)
|
|
139
|
+
* - `'either-pass'` — `run` returns a `Result` that travels forward whole. (decide)
|
|
140
|
+
* - `'total'` — `run` is a plain total function; call it directly. (encode)
|
|
141
|
+
*
|
|
142
|
+
* This is structural data on the record, not one of the five axes: it lets an executing
|
|
143
|
+
* consumer fold the description without knowing which phase it is looking at. The
|
|
144
|
+
* interpreter's switch over `convention` is exhaustively defaulted, so a phase with an
|
|
145
|
+
* invocation shape this module does not know fails at compile time at one named location.
|
|
146
|
+
*/
|
|
147
|
+
type Convention = 'effect' | 'either-fail' | 'either-pass' | 'total';
|
|
148
|
+
/**
|
|
149
|
+
* One phase record: `name` (the phase name, as data), `kind` (purity), `convention`
|
|
150
|
+
* (the invocation shape), and the phase's `run`. Each phase type has its own record
|
|
151
|
+
* interface so `run` keeps its exact signature. Nothing here discriminates on `name`:
|
|
152
|
+
* the interpreter dispatches on `convention`, and other consumers read `name` as data.
|
|
153
|
+
* A hand-written `_tag` is deliberately absent — a manual `_tag` member is forbidden
|
|
154
|
+
* here (the schema rule prescribes `TaggedStruct`, which cannot describe a function
|
|
155
|
+
* member), and an ordered sequence of phase records is not a `Match`-style tagged union.
|
|
156
|
+
*/
|
|
157
|
+
interface ReadNode<P extends Phases> {
|
|
158
|
+
readonly name: 'read';
|
|
159
|
+
readonly kind: 'impure';
|
|
160
|
+
readonly convention: 'effect';
|
|
161
|
+
readonly run: ReadPhase<P>;
|
|
162
|
+
}
|
|
163
|
+
interface DecodeNode<P extends Phases> {
|
|
164
|
+
readonly name: 'decode';
|
|
165
|
+
readonly kind: 'pure';
|
|
166
|
+
readonly convention: 'either-fail';
|
|
167
|
+
readonly run: DecodePhase<P>;
|
|
168
|
+
}
|
|
169
|
+
interface DecideNode<P extends Phases> {
|
|
170
|
+
readonly name: 'decide';
|
|
171
|
+
readonly kind: 'pure';
|
|
172
|
+
readonly convention: 'either-pass';
|
|
173
|
+
readonly run: DecidePhase<P>;
|
|
174
|
+
}
|
|
175
|
+
interface EncodeNode<P extends Phases> {
|
|
176
|
+
readonly name: 'encode';
|
|
177
|
+
readonly kind: 'pure';
|
|
178
|
+
readonly convention: 'total';
|
|
179
|
+
readonly run: EncodePhase<P>;
|
|
180
|
+
}
|
|
181
|
+
interface WriteNode<P extends Phases> {
|
|
182
|
+
readonly name: 'write';
|
|
183
|
+
readonly kind: 'impure';
|
|
184
|
+
readonly convention: 'effect';
|
|
185
|
+
readonly run: WritePhase<P>;
|
|
186
|
+
}
|
|
187
|
+
type Phase<P extends Phases> = ReadNode<P> | DecodeNode<P> | DecideNode<P> | EncodeNode<P> | WriteNode<P>;
|
|
188
|
+
/**
|
|
189
|
+
* One impure/pure layer: its phase records in intra-layer execution order. The order is
|
|
190
|
+
* data — the interpreter folds this array and runs each record as its `convention`
|
|
191
|
+
* says, holding no phase sequence of its own. The stage brands are what make a legal
|
|
192
|
+
* description well-ordered at build time; the value's declared order is its execution
|
|
193
|
+
* order.
|
|
194
|
+
*/
|
|
195
|
+
interface Layer<P extends Phases> {
|
|
196
|
+
readonly phases: readonly Phase<P>[];
|
|
197
|
+
}
|
|
198
|
+
/** The description package's own module name — what an import edge would match. */
|
|
199
|
+
declare const DESCRIPTION_MODULE: '@systemfsoftware/effect-cell-types';
|
|
200
|
+
/**
|
|
201
|
+
* The I/O-cell classification: the cells whose calls are I/O, plus the non-cell module
|
|
202
|
+
* sources whose calls are I/O. Written once here; a consumer folds it off the value.
|
|
203
|
+
*/
|
|
204
|
+
declare const IO_CELLS: {
|
|
205
|
+
readonly cells: readonly ['store', 'adapter'];
|
|
206
|
+
readonly sources: readonly ['effect/Clock', 'effect/System'];
|
|
207
|
+
};
|
|
208
|
+
/**
|
|
209
|
+
* Derived from the value rather than restated beside it: a hand-written twin is a second
|
|
210
|
+
* declaration of axis 5, and the two drift the moment a cell is reclassified in only one.
|
|
211
|
+
*/
|
|
212
|
+
type IoCellClassification = typeof IO_CELLS;
|
|
213
|
+
/**
|
|
214
|
+
* The description root, carried by every stage. A consumer folds a stage value to
|
|
215
|
+
* recover the description's whole vocabulary: the phase names and kinds on each
|
|
216
|
+
* record, the intra-layer order in each `phases` array, the package's own module
|
|
217
|
+
* name, and the I/O-cell classification. Nothing about the shape of a legal
|
|
218
|
+
* description is written down anywhere else.
|
|
219
|
+
*/
|
|
220
|
+
interface Description<P extends Phases> {
|
|
221
|
+
readonly module: typeof DESCRIPTION_MODULE;
|
|
222
|
+
readonly ioCells: IoCellClassification;
|
|
223
|
+
readonly layers: readonly Layer<P>[];
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The stages are siblings, never a hierarchy. Each carries exactly the sentence naming the
|
|
227
|
+
* call that must come next, and none extends another. Measured: under a hierarchy a later
|
|
228
|
+
* stage is assignable to an earlier parameter, so an inversion — decoding what was already
|
|
229
|
+
* decided — compiles. As siblings both the forward skip and the backward inversion are
|
|
230
|
+
* rejected, each diagnostic naming the sentence it is missing.
|
|
231
|
+
*
|
|
232
|
+
* The carrier is deliberately the same type on every stage — `Description` — so the
|
|
233
|
+
* compiler reports the argument rather than the member only when the carrier itself is
|
|
234
|
+
* wrong, and the sentence stays the name a diagnostic reports.
|
|
235
|
+
*/
|
|
236
|
+
interface ReadDone<P extends Phases> extends Description<P> {
|
|
237
|
+
readonly 'call read(command) before decode(raw)': true;
|
|
238
|
+
}
|
|
239
|
+
interface DecodeDone<P extends Phases> extends Description<P> {
|
|
240
|
+
readonly 'call decode(raw) before decide(decoded)': true;
|
|
241
|
+
}
|
|
242
|
+
interface DecideDone<P extends Phases> extends Description<P> {
|
|
243
|
+
readonly 'call decide(decoded) before encode(decision)': true;
|
|
244
|
+
}
|
|
245
|
+
interface EncodeDone<P extends Phases> extends Description<P> {
|
|
246
|
+
readonly 'call encode(decision) before write(output)': true;
|
|
247
|
+
}
|
|
248
|
+
/** Terminal. A description is applied from here, and a further layer opens from here. */
|
|
249
|
+
interface WriteDone<P extends Phases> extends Description<P> {
|
|
250
|
+
readonly 'call write(output) before applying the description': true;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Opens a layer. Passing a prior `WriteDone` opens a second layer over the same bag, so a
|
|
254
|
+
* call site whose real order writes before it can classify is one description carrying two
|
|
255
|
+
* layers rather than two descriptions composed by hand.
|
|
256
|
+
*
|
|
257
|
+
* This one is not dual: it starts the chain, so on the opening layer it has no `self` to
|
|
258
|
+
* receive. Every phase after it is dual, which is what lets a description be written in the
|
|
259
|
+
* order it runs.
|
|
260
|
+
*/
|
|
261
|
+
declare const read: <P extends Phases>(run: ReadPhase<P>, previous?: WriteDone<P>) => ReadDone<P>;
|
|
262
|
+
/**
|
|
263
|
+
* The chaining phases are dual, data-last overload declared first. Nesting the constructors
|
|
264
|
+
* reads innermost-first — backwards from the order the phases run — which defeats the point of
|
|
265
|
+
* a type that exists to make that order legible. In `pipe` the call site reads in phase order,
|
|
266
|
+
* and the sentence still arrives as a missing member through it.
|
|
267
|
+
*
|
|
268
|
+
* A `Do`-notation scope binding each phase's result for later phases to read was measured as
|
|
269
|
+
* an alternative: the sentence survives it, even with the scope varying per stage, because an
|
|
270
|
+
* absent member is reported before type arguments are compared. It was not adopted, because a
|
|
271
|
+
* scope the interpreter folds over is type-erased, and reading a phase back out of it needs an
|
|
272
|
+
* assertion this design does not.
|
|
273
|
+
*/
|
|
274
|
+
declare const decode: {
|
|
275
|
+
<P extends Phases>(run: DecodePhase<P>): (previous: ReadDone<P>) => DecodeDone<P>;
|
|
276
|
+
<P extends Phases>(previous: ReadDone<P>, run: DecodePhase<P>): DecodeDone<P>;
|
|
277
|
+
};
|
|
278
|
+
declare const decide: {
|
|
279
|
+
<P extends Phases>(run: DecidePhase<P>): (previous: DecodeDone<P>) => DecideDone<P>;
|
|
280
|
+
<P extends Phases>(previous: DecodeDone<P>, run: DecidePhase<P>): DecideDone<P>;
|
|
281
|
+
};
|
|
282
|
+
declare const encode: {
|
|
283
|
+
<P extends Phases>(run: EncodePhase<P>): (previous: DecideDone<P>) => EncodeDone<P>;
|
|
284
|
+
<P extends Phases>(previous: DecideDone<P>, run: EncodePhase<P>): EncodeDone<P>;
|
|
285
|
+
};
|
|
286
|
+
declare const write: {
|
|
287
|
+
<P extends Phases>(run: WritePhase<P>): (previous: EncodeDone<P>) => WriteDone<P>;
|
|
288
|
+
<P extends Phases>(previous: EncodeDone<P>, run: WritePhase<P>): WriteDone<P>;
|
|
289
|
+
};
|
|
290
|
+
/**
|
|
291
|
+
* Applies a description. The return type is deliberately not annotated: `gen` accumulates
|
|
292
|
+
* `E` and `R` from the union of what is actually yielded, so an over-claimed channel is
|
|
293
|
+
* unrepresentable rather than merely discouraged. Annotating it here would let this module
|
|
294
|
+
* promise a failure that no phase can produce.
|
|
295
|
+
*
|
|
296
|
+
* The parameter keeps the terminal `WriteDone<P>` brand. It is not decoration: it is what
|
|
297
|
+
* makes applying a half-built chain — a `ReadDone`, say — a compile error rather than a
|
|
298
|
+
* runtime death, which `test-types/Cell.tst.ts` pins. The brand does not constrain the
|
|
299
|
+
* phases array's order (a literal satisfies it in any order, which is why the interpreter
|
|
300
|
+
* reads the order off the value), but it does constrain chain completion, and that is a
|
|
301
|
+
* guarantee worth the narrower parameter.
|
|
302
|
+
*
|
|
303
|
+
* `forEach` takes no concurrency option here, so the layers run in declared order and the
|
|
304
|
+
* sequence is structural rather than something a caller could pass differently; the
|
|
305
|
+
* description's response is the last layer's. No scope is opened and interruptibility is
|
|
306
|
+
* untouched, so a `Scope.Scope` a phase requires reaches the caller as part of the derived `R`.
|
|
307
|
+
*/
|
|
308
|
+
declare const apply: <P extends Phases>(description: WriteDone<P>, command: P['command']) => Effect$1.Effect<P["response"], P["decodeError"] | P["readError"] | P["writeError"], P["readContext"] | P["writeContext"]>;
|
|
309
|
+
/** One phase's vocabulary entry: what it is called, its purity, its invocation shape. */
|
|
310
|
+
interface PhaseFact {
|
|
311
|
+
readonly name: Phase<Phases>['name'];
|
|
312
|
+
readonly kind: Phase<Phases>['kind'];
|
|
313
|
+
readonly convention: Convention;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* The five axes as data, for a consumer that has no description of its own to fold.
|
|
317
|
+
*
|
|
318
|
+
* `byKind` groups the walked phase names by their purity. It is here rather than left to
|
|
319
|
+
* each consumer because which phases are pure is this module's own fact, and a consumer
|
|
320
|
+
* that reconstructs it has to pick a proxy — inferring purity from the invocation shape,
|
|
321
|
+
* say — which is a different axis and silently disagrees the moment a pure phase is given
|
|
322
|
+
* an effectful shape or an impure one is not.
|
|
323
|
+
*/
|
|
324
|
+
interface Vocabulary {
|
|
325
|
+
readonly module: typeof DESCRIPTION_MODULE;
|
|
326
|
+
readonly ioCells: IoCellClassification;
|
|
327
|
+
readonly phases: readonly PhaseFact[];
|
|
328
|
+
readonly byKind: Readonly<Record<PhaseFact['kind'], readonly PhaseFact['name'][]>>;
|
|
329
|
+
/**
|
|
330
|
+
* The export that runs a finished description. A consumer deciding which calls on this module
|
|
331
|
+
* belong to a description needs the phases *and* the applier; without it the applier is the one
|
|
332
|
+
* name it has to write down for itself, and one restated name is enough to drift.
|
|
333
|
+
*/
|
|
334
|
+
readonly applier: 'apply';
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* A canonical description, built through the public constructors with phases that do
|
|
338
|
+
* nothing. It is exported so a consumer — a generator, a lint rule, a documenter — can
|
|
339
|
+
* obtain a real branded description without replaying the constructor chain: spread it
|
|
340
|
+
* and substitute its phase records' `run`s. The records it carries are the same literals
|
|
341
|
+
* the constructors write for real call sites, so the vocabulary below cannot drift from
|
|
342
|
+
* them.
|
|
343
|
+
*
|
|
344
|
+
* Its order is not a choice this module makes. The stage brands admit exactly one chain, so
|
|
345
|
+
* any other sequence fails to typecheck here — which is what keeps the derived order
|
|
346
|
+
* non-circular: it is read off a value, and the value's shape is enforced by the types.
|
|
347
|
+
*/
|
|
348
|
+
declare const canonical: WriteDone<Phases>;
|
|
349
|
+
declare const vocabulary: Vocabulary;
|
|
350
|
+
declare namespace Policy_d_exports {
|
|
351
|
+
export { Policy };
|
|
352
|
+
}
|
|
353
|
+
type Policy<A, E, R> = (self: Effect<A, E, R>) => Effect<A, E, R>;
|
|
354
|
+
declare namespace Wire_d_exports {
|
|
355
|
+
export { AnyMinted, Fields, Mark, Minted, MintedField, array, boolean, integer, literal, mint, nullOr, nullishOr, number, optional, record, refine, string, suspend, tuple, undefinedOr, union, wire };
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Marker a wire member carries once this workspace declares its type. The property type is the
|
|
359
|
+
* fix, so the compiler diagnostic names it.
|
|
360
|
+
*
|
|
361
|
+
* It sits on the schema, never on the decoded value: `Schema.Type<typeof Wire.string>` is `string`.
|
|
362
|
+
*/
|
|
363
|
+
interface Mark {
|
|
364
|
+
readonly __WIRE_MEMBER_IS_NOT_BUILT_FROM_THE_ALPHABET__: 'this member names a type this workspace does not declare; build it from Wire, or admit a foreign schema deliberately with Wire.mint';
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* A schema this workspace declares the type of. In v4 the encoded side lives on the schema
|
|
368
|
+
* itself, so the second parameter is carried for API stability rather than as a separate
|
|
369
|
+
* type argument: `Minted<A, I>` reads "decodes `A` from the wire's `I`-shaped payload".
|
|
370
|
+
*/
|
|
371
|
+
type Minted<A, I = A> = Schema.Codec<A, I> & Mark;
|
|
372
|
+
/** Any marked schema, for constraint positions. */
|
|
373
|
+
type AnyMinted = Schema.Top & Mark;
|
|
374
|
+
/**
|
|
375
|
+
* Any marked struct member — a schema, or a property signature from {@link optional}.
|
|
376
|
+
*
|
|
377
|
+
* The permissive `Constraint` arm rather than a narrower schema type, whose `never` variants
|
|
378
|
+
* would fail the assignability check before the marker is reached, leaving the diagnostic to
|
|
379
|
+
* report an unrelated type error.
|
|
380
|
+
*/
|
|
381
|
+
type MintedField = Schema.Constraint & Mark;
|
|
382
|
+
/**
|
|
383
|
+
* Mark a member whose type this workspace declares.
|
|
384
|
+
*
|
|
385
|
+
* Prefer the alphabet below; reach for this only to admit a schema the alphabet cannot express,
|
|
386
|
+
* including a vendor's own. The value returned is the one passed in, and its concrete type is
|
|
387
|
+
* preserved, so a marked struct stays a struct and Effect's inference keeps working.
|
|
388
|
+
*/
|
|
389
|
+
declare const mint: <Field extends Schema.Constraint>(field: Field) => Field & Mark;
|
|
390
|
+
declare const string: Schema.String & Mark;
|
|
391
|
+
declare const number: Schema.Finite & Mark;
|
|
392
|
+
declare const boolean: Schema.Boolean & Mark;
|
|
393
|
+
declare const integer: Schema.Int & Mark;
|
|
394
|
+
declare const literal: <const Literals extends ReadonlyArray<SchemaAST.LiteralValue>>(...literals: Literals) => Schema.Literals<Literals> & Mark;
|
|
395
|
+
declare const nullOr: <A, I>(member: Minted<A, I>) => Minted<A | null, I | null>;
|
|
396
|
+
declare const undefinedOr: <A, I>(member: Minted<A, I>) => Minted<A | undefined, I | undefined>;
|
|
397
|
+
declare const nullishOr: <A, I>(member: Minted<A, I>) => Minted<A | null | undefined, I | null | undefined>;
|
|
398
|
+
declare const array: <A, I>(member: Minted<A, I>) => Minted<ReadonlyArray<A>, ReadonlyArray<I>>;
|
|
399
|
+
/** A field that may be absent from the payload entirely, as distinct from present and `undefined`. */
|
|
400
|
+
declare const optional: <Member extends AnyMinted>(member: Member) => Schema.optional<Member> & Mark;
|
|
401
|
+
declare const record: <K extends Schema.Record.Key & Mark, V extends AnyMinted>(key: K, value: V) => Schema.$Record<K, V> & Mark;
|
|
402
|
+
declare const union: <Members extends readonly [AnyMinted, AnyMinted, ...Array<AnyMinted>]>(...members: Members) => Schema.Union<Members> & Mark;
|
|
403
|
+
declare const tuple: <Elements extends ReadonlyArray<AnyMinted>>(...elements: Elements) => Schema.Tuple<Elements> & Mark;
|
|
404
|
+
declare const suspend: <A, I>(thunk: () => Minted<A, I>) => Minted<A, I>;
|
|
405
|
+
/**
|
|
406
|
+
* Constrain a member's values. Effect's own `member.pipe(Schema.check(Schema.isMinLength(1)))`
|
|
407
|
+
* preserves the mark, but returns the schema with the concrete decode/encode pair fused; this
|
|
408
|
+
* combinator is the alphabet's own shape for the same operation, so a refined member keeps the
|
|
409
|
+
* declaration this workspace makes of its type.
|
|
410
|
+
*
|
|
411
|
+
* `S.refine` needs a type-guard as its predicate argument; the workspace's predicate is a plain
|
|
412
|
+
* `(a: A) => boolean` that narrows nothing, so a truth-preserving `value is A` guard is written
|
|
413
|
+
* at this one call site rather than widening the public signature.
|
|
414
|
+
*/
|
|
415
|
+
declare const refine: <A, I>(member: Minted<A, I>, predicate: (a: A) => boolean, annotations?: Schema.Annotations.Filter) => Minted<A, I>;
|
|
416
|
+
type Fields = Record<string, MintedField>;
|
|
417
|
+
declare const wire: <F extends Fields>(fields: F) => Schema.Struct<F> & Mark;
|
|
418
|
+
//#endregion
|
|
419
|
+
export { Cell_d_exports as Cell, Policy_d_exports as Policy, Wire_d_exports as Wire, Workflow_d_exports as Workflow };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import * as Arr from "effect/Array";
|
|
3
|
+
import * as Effect$1 from "effect/Effect";
|
|
4
|
+
import { dual } from "effect/Function";
|
|
5
|
+
import * as Option from "effect/Option";
|
|
6
|
+
import * as Result$1 from "effect/Result";
|
|
7
|
+
import { Schema } from "effect";
|
|
8
|
+
//#region src/Workflow.ts
|
|
9
|
+
var Workflow_exports = /* @__PURE__ */ __exportAll({ make: () => make });
|
|
10
|
+
/**
|
|
11
|
+
* Builds a workflow, refusing an uninhabited or untagged channel at this call rather than at
|
|
12
|
+
* whoever first calls the result — which for a workflow nothing calls yet is never.
|
|
13
|
+
*
|
|
14
|
+
* The markers ride the parameter function's return type, not the parameter as `Workflow<C, D, E>`:
|
|
15
|
+
* a conditional type in parameter position resolves `D` and `E` to `unknown` and the markers become
|
|
16
|
+
* unreachable. On the return type both still infer from the `Result` conjunct while the marker
|
|
17
|
+
* conjunct is what an uninhabited channel fails to satisfy.
|
|
18
|
+
*
|
|
19
|
+
* `E` carries no constraint on purpose. Constraining it gives inference a fallback: where `E` would
|
|
20
|
+
* infer as `never` TypeScript substitutes the constraint instead, the conditional takes its
|
|
21
|
+
* inhabited branch, and a `never` channel passes. The tagged requirement therefore lives in
|
|
22
|
+
* {@link Inhabited}, where nothing can stand in for `never`.
|
|
23
|
+
*
|
|
24
|
+
* The narrowing goes through an assertion signature rather than an `as` cast: every narrowing
|
|
25
|
+
* assertion trips `typescript(no-unsafe-type-assertion)`, and a suppression comment would hide the
|
|
26
|
+
* one place this file could lie. It is sound rather than merely permitted — with both channels
|
|
27
|
+
* inhabited `Workflow<C, D, E>` is `(command: C) => Result<D, E>` carrying the {@link WorkflowBrand}
|
|
28
|
+
* conjunct, and otherwise the return type is a marker with no call signature, so the value handed
|
|
29
|
+
* back is unobservable through it. The brand is applied here and nowhere else: the assertion adds
|
|
30
|
+
* no runtime property, so `make` stays the identity function it always was, yet a value that did
|
|
31
|
+
* not pass through this door fails the conjunct wherever a decision is run.
|
|
32
|
+
*/
|
|
33
|
+
const make = (decide) => {
|
|
34
|
+
return decide;
|
|
35
|
+
};
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/CanonicalDecide.workflow.ts
|
|
38
|
+
/**
|
|
39
|
+
* The canonical decider. Extracted so `make-file-location` only sees it inside a
|
|
40
|
+
* single-segment `.workflow.ts` file, satisfying the restored taxonomy while
|
|
41
|
+
* preserving the exact phantom-channel contract the `DecidePhase` brand and
|
|
42
|
+
* the interpreter rely on.
|
|
43
|
+
*/
|
|
44
|
+
const canonicalDecide = make((_decoded) => Result$1.succeed(void 0));
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/Cell.ts
|
|
47
|
+
var Cell_exports = /* @__PURE__ */ __exportAll({
|
|
48
|
+
DESCRIPTION_MODULE: () => DESCRIPTION_MODULE,
|
|
49
|
+
IO_CELLS: () => IO_CELLS,
|
|
50
|
+
apply: () => apply,
|
|
51
|
+
canonical: () => canonical,
|
|
52
|
+
decide: () => decide,
|
|
53
|
+
decode: () => decode,
|
|
54
|
+
encode: () => encode,
|
|
55
|
+
read: () => read,
|
|
56
|
+
vocabulary: () => vocabulary,
|
|
57
|
+
write: () => write
|
|
58
|
+
});
|
|
59
|
+
/** The description package's own module name — what an import edge would match. */
|
|
60
|
+
const DESCRIPTION_MODULE = "@systemfsoftware/effect-cell-types";
|
|
61
|
+
/**
|
|
62
|
+
* The I/O-cell classification: the cells whose calls are I/O, plus the non-cell module
|
|
63
|
+
* sources whose calls are I/O. Written once here; a consumer folds it off the value.
|
|
64
|
+
*/
|
|
65
|
+
const IO_CELLS = {
|
|
66
|
+
cells: ["store", "adapter"],
|
|
67
|
+
sources: ["effect/Clock", "effect/System"]
|
|
68
|
+
};
|
|
69
|
+
const READ_DONE = "call read(command) before decode(raw)";
|
|
70
|
+
const DECODE_DONE = "call decode(raw) before decide(decoded)";
|
|
71
|
+
const DECIDE_DONE = "call decide(decoded) before encode(decision)";
|
|
72
|
+
const ENCODE_DONE = "call encode(decision) before write(output)";
|
|
73
|
+
const WRITE_DONE = "call write(output) before applying the description";
|
|
74
|
+
/** Replaces the open layer with itself plus one more phase record. */
|
|
75
|
+
const intoOpenLayer = (description, phase) => {
|
|
76
|
+
const layers = description.layers;
|
|
77
|
+
const last = layers[layers.length - 1];
|
|
78
|
+
return {
|
|
79
|
+
module: description.module,
|
|
80
|
+
ioCells: description.ioCells,
|
|
81
|
+
layers: [...layers.slice(0, -1), {
|
|
82
|
+
...last,
|
|
83
|
+
phases: [...last?.phases ?? [], phase]
|
|
84
|
+
}]
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Opens a layer. Passing a prior `WriteDone` opens a second layer over the same bag, so a
|
|
89
|
+
* call site whose real order writes before it can classify is one description carrying two
|
|
90
|
+
* layers rather than two descriptions composed by hand.
|
|
91
|
+
*
|
|
92
|
+
* This one is not dual: it starts the chain, so on the opening layer it has no `self` to
|
|
93
|
+
* receive. Every phase after it is dual, which is what lets a description be written in the
|
|
94
|
+
* order it runs.
|
|
95
|
+
*/
|
|
96
|
+
const read = (run, previous) => ({
|
|
97
|
+
[READ_DONE]: true,
|
|
98
|
+
module: DESCRIPTION_MODULE,
|
|
99
|
+
ioCells: IO_CELLS,
|
|
100
|
+
layers: [...previous?.layers ?? [], { phases: [{
|
|
101
|
+
name: "read",
|
|
102
|
+
kind: "impure",
|
|
103
|
+
convention: "effect",
|
|
104
|
+
run
|
|
105
|
+
}] }]
|
|
106
|
+
});
|
|
107
|
+
/**
|
|
108
|
+
* The chaining phases are dual, data-last overload declared first. Nesting the constructors
|
|
109
|
+
* reads innermost-first — backwards from the order the phases run — which defeats the point of
|
|
110
|
+
* a type that exists to make that order legible. In `pipe` the call site reads in phase order,
|
|
111
|
+
* and the sentence still arrives as a missing member through it.
|
|
112
|
+
*
|
|
113
|
+
* A `Do`-notation scope binding each phase's result for later phases to read was measured as
|
|
114
|
+
* an alternative: the sentence survives it, even with the scope varying per stage, because an
|
|
115
|
+
* absent member is reported before type arguments are compared. It was not adopted, because a
|
|
116
|
+
* scope the interpreter folds over is type-erased, and reading a phase back out of it needs an
|
|
117
|
+
* assertion this design does not.
|
|
118
|
+
*/
|
|
119
|
+
const decode = dual(2, (previous, run) => ({
|
|
120
|
+
[DECODE_DONE]: true,
|
|
121
|
+
...intoOpenLayer(previous, {
|
|
122
|
+
name: "decode",
|
|
123
|
+
kind: "pure",
|
|
124
|
+
convention: "either-fail",
|
|
125
|
+
run
|
|
126
|
+
})
|
|
127
|
+
}));
|
|
128
|
+
const decide = dual(2, (previous, run) => ({
|
|
129
|
+
[DECIDE_DONE]: true,
|
|
130
|
+
...intoOpenLayer(previous, {
|
|
131
|
+
name: "decide",
|
|
132
|
+
kind: "pure",
|
|
133
|
+
convention: "either-pass",
|
|
134
|
+
run
|
|
135
|
+
})
|
|
136
|
+
}));
|
|
137
|
+
const encode = dual(2, (previous, run) => ({
|
|
138
|
+
[ENCODE_DONE]: true,
|
|
139
|
+
...intoOpenLayer(previous, {
|
|
140
|
+
name: "encode",
|
|
141
|
+
kind: "pure",
|
|
142
|
+
convention: "total",
|
|
143
|
+
run
|
|
144
|
+
})
|
|
145
|
+
}));
|
|
146
|
+
const write = dual(2, (previous, run) => ({
|
|
147
|
+
[WRITE_DONE]: true,
|
|
148
|
+
...intoOpenLayer(previous, {
|
|
149
|
+
name: "write",
|
|
150
|
+
kind: "impure",
|
|
151
|
+
convention: "effect",
|
|
152
|
+
run
|
|
153
|
+
})
|
|
154
|
+
}));
|
|
155
|
+
/**
|
|
156
|
+
* The runtime guard that lets the `'total'` case call `EncodePhase` soundly: an encode
|
|
157
|
+
* phase is chained only after a decide, so the value reaching it is the outcome `Result`
|
|
158
|
+
* and `Result.isResult` certifies exactly that. The specific `decision`/`decisionError`
|
|
159
|
+
* members are not observable at runtime, so the guard narrows to them by construction —
|
|
160
|
+
* the same trust the fold places in the chain's order.
|
|
161
|
+
*/
|
|
162
|
+
const isOutcome = (value) => Result$1.isResult(value);
|
|
163
|
+
/**
|
|
164
|
+
* Runs one layer as the sandwich the value declares: the phase records run in array
|
|
165
|
+
* order, each dispatched on its carried `convention`. There is no phase sequence here —
|
|
166
|
+
* the order is the `phases` array and the invocation shape is the `convention` field,
|
|
167
|
+
* so a description's declared order IS its execution order. The convention switch is
|
|
168
|
+
* exhaustive over the union via its `never` default: a future phase with an invocation
|
|
169
|
+
* shape this module does not know fails at compile time, at this one location.
|
|
170
|
+
*
|
|
171
|
+
* The two `Failure` rules are carried by the phase types rather than chosen here. A `decode`
|
|
172
|
+
* Failure has no downstream consumer — nothing accepts `decodeError` — so its only route is a
|
|
173
|
+
* failure, which is what puts it in the derived error channel. A `decide` Failure cannot be
|
|
174
|
+
* unwrapped, because `EncodePhase` takes the whole `Result`, so its only route is forward as
|
|
175
|
+
* a value. Neither is a decision the interpreter makes.
|
|
176
|
+
*
|
|
177
|
+
* Every layer reachable from a `WriteDone` was built by the five constructors in order, so
|
|
178
|
+
* its last phase is a write and every slot is filled. A layer that is nonetheless empty or
|
|
179
|
+
* not closed by a write is a defect in this module, never a domain outcome, so it dies —
|
|
180
|
+
* the same guard the name-keyed layer used for unfilled slots. `Effect.die` returns
|
|
181
|
+
* `Effect<never>`, which is why the guards cost the derived `E` and `R` nothing.
|
|
182
|
+
*/
|
|
183
|
+
const runLayer = (layer, command) => Effect$1.gen(function* () {
|
|
184
|
+
const phases = layer.phases;
|
|
185
|
+
const last = phases[phases.length - 1];
|
|
186
|
+
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"));
|
|
187
|
+
let value = command;
|
|
188
|
+
for (const phase of phases.slice(0, -1)) switch (phase.convention) {
|
|
189
|
+
case "effect":
|
|
190
|
+
value = yield* phase.run(value);
|
|
191
|
+
break;
|
|
192
|
+
case "either-fail":
|
|
193
|
+
value = yield* Result$1.match(phase.run(value), {
|
|
194
|
+
onFailure: Effect$1.fail,
|
|
195
|
+
onSuccess: Effect$1.succeed
|
|
196
|
+
});
|
|
197
|
+
break;
|
|
198
|
+
case "either-pass":
|
|
199
|
+
value = phase.run(value);
|
|
200
|
+
break;
|
|
201
|
+
case "total":
|
|
202
|
+
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"));
|
|
203
|
+
value = phase.run(value);
|
|
204
|
+
break;
|
|
205
|
+
default: {
|
|
206
|
+
const unreachable = phase;
|
|
207
|
+
return yield* Effect$1.die(/* @__PURE__ */ new Error(`effect-cell-types: unknown phase convention ${String(unreachable)}`));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return yield* last.run(value);
|
|
211
|
+
});
|
|
212
|
+
/**
|
|
213
|
+
* Applies a description. The return type is deliberately not annotated: `gen` accumulates
|
|
214
|
+
* `E` and `R` from the union of what is actually yielded, so an over-claimed channel is
|
|
215
|
+
* unrepresentable rather than merely discouraged. Annotating it here would let this module
|
|
216
|
+
* promise a failure that no phase can produce.
|
|
217
|
+
*
|
|
218
|
+
* The parameter keeps the terminal `WriteDone<P>` brand. It is not decoration: it is what
|
|
219
|
+
* makes applying a half-built chain — a `ReadDone`, say — a compile error rather than a
|
|
220
|
+
* runtime death, which `test-types/Cell.tst.ts` pins. The brand does not constrain the
|
|
221
|
+
* phases array's order (a literal satisfies it in any order, which is why the interpreter
|
|
222
|
+
* reads the order off the value), but it does constrain chain completion, and that is a
|
|
223
|
+
* guarantee worth the narrower parameter.
|
|
224
|
+
*
|
|
225
|
+
* `forEach` takes no concurrency option here, so the layers run in declared order and the
|
|
226
|
+
* sequence is structural rather than something a caller could pass differently; the
|
|
227
|
+
* description's response is the last layer's. No scope is opened and interruptibility is
|
|
228
|
+
* untouched, so a `Scope.Scope` a phase requires reaches the caller as part of the derived `R`.
|
|
229
|
+
*/
|
|
230
|
+
const apply = (description, command) => Effect$1.gen(function* () {
|
|
231
|
+
const responses = yield* Effect$1.forEach(description.layers, (layer) => runLayer(layer, command));
|
|
232
|
+
return yield* Option.match(Arr.last(responses), {
|
|
233
|
+
onNone: () => Effect$1.die(/* @__PURE__ */ new Error("effect-cell-types: a description reached the interpreter with no layers")),
|
|
234
|
+
onSome: Effect$1.succeed
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
/**
|
|
238
|
+
* A canonical description, built through the public constructors with phases that do
|
|
239
|
+
* nothing. It is exported so a consumer — a generator, a lint rule, a documenter — can
|
|
240
|
+
* obtain a real branded description without replaying the constructor chain: spread it
|
|
241
|
+
* and substitute its phase records' `run`s. The records it carries are the same literals
|
|
242
|
+
* the constructors write for real call sites, so the vocabulary below cannot drift from
|
|
243
|
+
* them.
|
|
244
|
+
*
|
|
245
|
+
* Its order is not a choice this module makes. The stage brands admit exactly one chain, so
|
|
246
|
+
* any other sequence fails to typecheck here — which is what keeps the derived order
|
|
247
|
+
* non-circular: it is read off a value, and the value's shape is enforced by the types.
|
|
248
|
+
*/
|
|
249
|
+
const canonical = write(encode(decide(decode(read(() => Effect$1.void), () => Result$1.succeed(void 0)), canonicalDecide), () => void 0), () => Effect$1.void);
|
|
250
|
+
/**
|
|
251
|
+
* The phase vocabulary, obtained by walking `canonical`. A consumer that needs the phase
|
|
252
|
+
* names, their purity, or their order — a lint rule, a generator, a document — reads them
|
|
253
|
+
* from here instead of restating them, so there is one place a phase is described and the
|
|
254
|
+
* description is the place.
|
|
255
|
+
*/
|
|
256
|
+
const WALKED_PHASES = Arr.flatMap(canonical.layers, (layer) => layer.phases.map(({ convention, kind, name }) => ({
|
|
257
|
+
convention,
|
|
258
|
+
kind,
|
|
259
|
+
name
|
|
260
|
+
})));
|
|
261
|
+
const vocabulary = {
|
|
262
|
+
module: canonical.module,
|
|
263
|
+
ioCells: canonical.ioCells,
|
|
264
|
+
phases: WALKED_PHASES,
|
|
265
|
+
byKind: {
|
|
266
|
+
pure: WALKED_PHASES.filter((phase) => phase.kind === "pure").map((phase) => phase.name),
|
|
267
|
+
impure: WALKED_PHASES.filter((phase) => phase.kind === "impure").map((phase) => phase.name)
|
|
268
|
+
},
|
|
269
|
+
applier: "apply"
|
|
270
|
+
};
|
|
271
|
+
//#endregion
|
|
272
|
+
//#region src/Policy.ts
|
|
273
|
+
var Policy_exports = /* @__PURE__ */ __exportAll({});
|
|
274
|
+
//#endregion
|
|
275
|
+
//#region src/Wire.ts
|
|
276
|
+
/**
|
|
277
|
+
* The wire alphabet — build a schema for a foreign payload out of members you declare, so no
|
|
278
|
+
* vendor type can be named inside it.
|
|
279
|
+
*
|
|
280
|
+
* ```ts
|
|
281
|
+
* import { Wire } from '@systemfsoftware/effect-cell-types'
|
|
282
|
+
* import { Schema } from 'effect'
|
|
283
|
+
*
|
|
284
|
+
* const Invoice = Wire.wire({
|
|
285
|
+
* id: Wire.string,
|
|
286
|
+
* amountDue: Wire.nullOr(Wire.number),
|
|
287
|
+
* status: Wire.literal('draft', 'open', 'paid'),
|
|
288
|
+
* lineItems: Wire.array(Wire.string),
|
|
289
|
+
* metadata: Wire.record(Wire.string, Wire.string),
|
|
290
|
+
* deleted: Wire.optional(Wire.boolean),
|
|
291
|
+
* })
|
|
292
|
+
*
|
|
293
|
+
* // Decodes to your own type — `{ id: string; amountDue: number | null; ... }`.
|
|
294
|
+
* const invoice = Schema.decodeUnknownSync(Invoice)(payload)
|
|
295
|
+
* ```
|
|
296
|
+
*
|
|
297
|
+
* Only declarations that use the alphabet are constrained, and {@link mint} admits a foreign
|
|
298
|
+
* schema deliberately. It is a guardrail, not a security boundary.
|
|
299
|
+
*/
|
|
300
|
+
var Wire_exports = /* @__PURE__ */ __exportAll({
|
|
301
|
+
array: () => array,
|
|
302
|
+
boolean: () => boolean,
|
|
303
|
+
integer: () => integer,
|
|
304
|
+
literal: () => literal,
|
|
305
|
+
mint: () => mint,
|
|
306
|
+
nullOr: () => nullOr,
|
|
307
|
+
nullishOr: () => nullishOr,
|
|
308
|
+
number: () => number,
|
|
309
|
+
optional: () => optional,
|
|
310
|
+
record: () => record,
|
|
311
|
+
refine: () => refine,
|
|
312
|
+
string: () => string,
|
|
313
|
+
suspend: () => suspend,
|
|
314
|
+
tuple: () => tuple,
|
|
315
|
+
undefinedOr: () => undefinedOr,
|
|
316
|
+
union: () => union,
|
|
317
|
+
wire: () => wire
|
|
318
|
+
});
|
|
319
|
+
/**
|
|
320
|
+
* Mark a member whose type this workspace declares.
|
|
321
|
+
*
|
|
322
|
+
* Prefer the alphabet below; reach for this only to admit a schema the alphabet cannot express,
|
|
323
|
+
* including a vendor's own. The value returned is the one passed in, and its concrete type is
|
|
324
|
+
* preserved, so a marked struct stays a struct and Effect's inference keeps working.
|
|
325
|
+
*/
|
|
326
|
+
const mint = (field) => {
|
|
327
|
+
assertMinted(field);
|
|
328
|
+
return field;
|
|
329
|
+
};
|
|
330
|
+
function assertMinted(_field) {}
|
|
331
|
+
const string = mint(Schema.String);
|
|
332
|
+
const number = mint(Schema.Finite);
|
|
333
|
+
const boolean = mint(Schema.Boolean);
|
|
334
|
+
const integer = mint(Schema.Int);
|
|
335
|
+
const literal = (...literals) => mint(Schema.Literals(literals));
|
|
336
|
+
const nullOr = (member) => mint(Schema.NullOr(member));
|
|
337
|
+
const undefinedOr = (member) => mint(Schema.UndefinedOr(member));
|
|
338
|
+
const nullishOr = (member) => mint(Schema.NullishOr(member));
|
|
339
|
+
const array = (member) => mint(Schema.Array(member));
|
|
340
|
+
/** A field that may be absent from the payload entirely, as distinct from present and `undefined`. */
|
|
341
|
+
const optional = (member) => mint(Schema.optional(member));
|
|
342
|
+
const record = (key, value) => mint(Schema.Record(key, value));
|
|
343
|
+
const union = (...members) => mint(Schema.Union(members));
|
|
344
|
+
const tuple = (...elements) => mint(Schema.Tuple(elements));
|
|
345
|
+
const suspend = (thunk) => mint(Schema.suspend(thunk));
|
|
346
|
+
/**
|
|
347
|
+
* Constrain a member's values. Effect's own `member.pipe(Schema.check(Schema.isMinLength(1)))`
|
|
348
|
+
* preserves the mark, but returns the schema with the concrete decode/encode pair fused; this
|
|
349
|
+
* combinator is the alphabet's own shape for the same operation, so a refined member keeps the
|
|
350
|
+
* declaration this workspace makes of its type.
|
|
351
|
+
*
|
|
352
|
+
* `S.refine` needs a type-guard as its predicate argument; the workspace's predicate is a plain
|
|
353
|
+
* `(a: A) => boolean` that narrows nothing, so a truth-preserving `value is A` guard is written
|
|
354
|
+
* at this one call site rather than widening the public signature.
|
|
355
|
+
*/
|
|
356
|
+
const refine = (member, predicate, annotations) => mint(Schema.refine((value) => predicate(value), annotations)(member));
|
|
357
|
+
const wire = (fields) => mint(Schema.Struct(fields));
|
|
358
|
+
//#endregion
|
|
359
|
+
export { Cell_exports as Cell, Policy_exports as Policy, Wire_exports as Wire, Workflow_exports as Workflow };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { __exportAll as t };
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@systemfsoftware/effect-cell-types",
|
|
3
|
+
"license": "Apache-2.0",
|
|
4
|
+
"version": "1.0.1",
|
|
5
|
+
"author": "Ryan Lee <drdgvhbh@gmail.com>",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/systemfsoftware/systemfsoftware.git",
|
|
9
|
+
"directory": "packages/effect-cell-types"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/systemfsoftware/systemfsoftware/tree/main/packages/effect-cell-types#readme",
|
|
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.",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"effect",
|
|
16
|
+
"effect-ts",
|
|
17
|
+
"typescript",
|
|
18
|
+
"architecture",
|
|
19
|
+
"workflow",
|
|
20
|
+
"either",
|
|
21
|
+
"type-contracts"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"default": "./dist/index.mjs"
|
|
28
|
+
},
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@effect/vitest": "4.0.0-rc.108",
|
|
36
|
+
"@microsoft/api-extractor": "^7.58.7",
|
|
37
|
+
"@systemfsoftware/arethetypeswrong-cli": "^1.1.1",
|
|
38
|
+
"@types/node": "^24",
|
|
39
|
+
"effect": "4.0.0-rc.108",
|
|
40
|
+
"oxlint": "^1.77.0",
|
|
41
|
+
"rimraf": "^6.1.3",
|
|
42
|
+
"tsdown": "^0.22.14",
|
|
43
|
+
"tstyche": "^7.1.0",
|
|
44
|
+
"vitest": "^4.1.10",
|
|
45
|
+
"@systemfsoftware/oxlint-config": "^0.1.0",
|
|
46
|
+
"@systemfsoftware/effect-gherkin-spec": "0.5.1",
|
|
47
|
+
"@systemfsoftware/vitest-config": "^0.1.0",
|
|
48
|
+
"@systemfsoftware/tsconfig": "^1.3.1"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"effect": "4.0.0-rc.108"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"provenance": true
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"clean": "rimraf dist",
|
|
58
|
+
"build": "tsdown && pnpm api:check",
|
|
59
|
+
"typecheck": "tsc --noEmit --incremental",
|
|
60
|
+
"test": "vitest run --passWithNoTests",
|
|
61
|
+
"test:run": "vitest run --passWithNoTests",
|
|
62
|
+
"test:types": "TSTYCHE_TYPESCRIPT_MODULE=tstyche-typescript tstyche",
|
|
63
|
+
"api:check": "api-extractor run",
|
|
64
|
+
"api:update": "api-extractor run --local",
|
|
65
|
+
"lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}",
|
|
66
|
+
"attw": "attw --pack ."
|
|
67
|
+
}
|
|
68
|
+
}
|